mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
21 Commits
feat/ttyd-
...
v0.0.59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02e9ed3181 | ||
|
|
78b07473f5 | ||
|
|
f562ed53e2 | ||
|
|
a1507aefdc | ||
|
|
ae66eb6a64 | ||
|
|
432c7cc889 | ||
|
|
0b138d9d49 | ||
|
|
c34e066a3b | ||
|
|
449c6791bd | ||
|
|
2b67ac084b | ||
|
|
76de8a48fc | ||
|
|
a80505bbfb | ||
|
|
af23644a50 | ||
|
|
98e6a902bf | ||
|
|
8b2bd6d04f | ||
|
|
4f4f43f044 | ||
|
|
8a5d751740 | ||
|
|
bc423b47f5 | ||
|
|
6d5c92076b | ||
|
|
fec554fc7c | ||
|
|
59ca6e42d9 |
13
README.md
13
README.md
@@ -14,6 +14,19 @@ A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs an
|
|||||||
- 📋 **Progress Tracking**: Visual progress indicators with checkboxes that dynamically update as Claude completes tasks
|
- 📋 **Progress Tracking**: Visual progress indicators with checkboxes that dynamically update as Claude completes tasks
|
||||||
- 🏃 **Runs on Your Infrastructure**: The action executes entirely on your own GitHub runner (Anthropic API calls go to your chosen provider)
|
- 🏃 **Runs on Your Infrastructure**: The action executes entirely on your own GitHub runner (Anthropic API calls go to your chosen provider)
|
||||||
|
|
||||||
|
## ⚠️ **BREAKING CHANGES COMING IN v1.0** ⚠️
|
||||||
|
|
||||||
|
**We're planning a major update that will significantly change how this action works.** The new version will:
|
||||||
|
|
||||||
|
- ✨ Automatically select the appropriate mode (no more `mode` input)
|
||||||
|
- 🔧 Simplify configuration with unified `prompt` and `claude_args`
|
||||||
|
- 🚀 Align more closely with the Claude Code SDK capabilities
|
||||||
|
- 💥 Remove multiple inputs like `direct_prompt`, `custom_instructions`, and others
|
||||||
|
|
||||||
|
**[→ Read the full v1.0 roadmap and provide feedback](https://github.com/anthropics/claude-code-action/discussions/428)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
The easiest way to set up this action is through [Claude Code](https://claude.ai/code) in the terminal. Just open `claude` and run `/install-github-app`.
|
The easiest way to set up this action is through [Claude Code](https://claude.ai/code) in the terminal. Just open `claude` and run `/install-github-app`.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Thank you for trying out the beta of our GitHub Action! This document outlines o
|
|||||||
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
|
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
|
||||||
- **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
|
- **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
|
||||||
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
|
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
|
||||||
- **Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude
|
- ~**Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude~
|
||||||
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data
|
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ inputs:
|
|||||||
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
||||||
required: false
|
required: false
|
||||||
default: "claude/"
|
default: "claude/"
|
||||||
|
allowed_bots:
|
||||||
|
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
# Mode configuration
|
# Mode configuration
|
||||||
mode:
|
mode:
|
||||||
@@ -156,6 +160,7 @@ runs:
|
|||||||
OVERRIDE_PROMPT: ${{ inputs.override_prompt }}
|
OVERRIDE_PROMPT: ${{ inputs.override_prompt }}
|
||||||
MCP_CONFIG: ${{ inputs.mcp_config }}
|
MCP_CONFIG: ${{ inputs.mcp_config }}
|
||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
|
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||||
@@ -172,7 +177,8 @@ runs:
|
|||||||
echo "Base-action dependencies installed"
|
echo "Base-action dependencies installed"
|
||||||
cd -
|
cd -
|
||||||
# Install Claude Code globally
|
# Install Claude Code globally
|
||||||
bun install -g @anthropic-ai/claude-code@1.0.70
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Setup Network Restrictions
|
- name: Setup Network Restrictions
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Claude Code
|
- name: Install Claude Code
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bun install -g @anthropic-ai/claude-code@1.0.70
|
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
||||||
|
|
||||||
- name: Run Claude Code Action
|
- name: Run Claude Code Action
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -207,15 +207,8 @@ Claude does **not** have access to execute arbitrary Bash commands by default. I
|
|||||||
```yaml
|
```yaml
|
||||||
- uses: anthropics/claude-code-action@beta
|
- uses: anthropics/claude-code-action@beta
|
||||||
with:
|
with:
|
||||||
allowed_tools: |
|
allowed_tools: "Bash(npm install),Bash(npm run test),Edit,Replace,NotebookEditCell"
|
||||||
Bash(npm install)
|
disallowed_tools: "TaskOutput,KillTask"
|
||||||
Bash(npm run test)
|
|
||||||
Edit
|
|
||||||
Replace
|
|
||||||
NotebookEditCell
|
|
||||||
disallowed_tools: |
|
|
||||||
TaskOutput
|
|
||||||
KillTask
|
|
||||||
# ... other inputs
|
# ... other inputs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Access Control
|
## Access Control
|
||||||
|
|
||||||
- **Repository Access**: The action can only be triggered by users with write access to the repository
|
- **Repository Access**: The action can only be triggered by users with write access to the repository
|
||||||
- **No Bot Triggers**: GitHub Apps and bots cannot trigger this action
|
- **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots
|
||||||
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
|
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
|
||||||
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
|
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
|
||||||
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ jobs:
|
|||||||
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
|
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
|
||||||
# additional_permissions: |
|
# additional_permissions: |
|
||||||
# actions: read
|
# actions: read
|
||||||
|
# Optional: allow bot users to trigger the action
|
||||||
|
# allowed_bots: "dependabot[bot],renovate[bot]"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
@@ -76,6 +78,7 @@ jobs:
|
|||||||
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
||||||
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
||||||
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||||
|
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
|
||||||
|
|
||||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Claude PR Assistant
|
name: Claude Code
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
@@ -11,38 +11,53 @@ on:
|
|||||||
types: [submitted]
|
types: [submitted]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
claude-code-action:
|
claude:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||||
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
issues: read
|
issues: write
|
||||||
id-token: write
|
id-token: write
|
||||||
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Run Claude PR Action
|
- name: Run Claude Code
|
||||||
|
id: claude
|
||||||
uses: anthropics/claude-code-action@beta
|
uses: anthropics/claude-code-action@beta
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
# Or use OAuth token instead:
|
|
||||||
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
# This is an optional setting that allows Claude to read CI results on PRs
|
||||||
timeout_minutes: "60"
|
additional_permissions: |
|
||||||
# mode: tag # Default: responds to @claude mentions
|
actions: read
|
||||||
# Optional: Restrict network access to specific domains only
|
|
||||||
# experimental_allowed_domains: |
|
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
|
||||||
# .anthropic.com
|
# model: "claude-opus-4-1-20250805"
|
||||||
# .github.com
|
|
||||||
# api.github.com
|
# Optional: Customize the trigger phrase (default: @claude)
|
||||||
# .githubusercontent.com
|
# trigger_phrase: "/claude"
|
||||||
# bun.sh
|
|
||||||
# registry.npmjs.org
|
# Optional: Trigger when specific user is assigned to an issue
|
||||||
# .blob.core.windows.net
|
# assignee_trigger: "claude-bot"
|
||||||
|
|
||||||
|
# Optional: Allow Claude to run specific commands
|
||||||
|
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
||||||
|
|
||||||
|
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
||||||
|
# custom_instructions: |
|
||||||
|
# Follow our coding standards
|
||||||
|
# Ensure all new code has tests
|
||||||
|
# Use TypeScript for new files
|
||||||
|
|
||||||
|
# Optional: Custom environment variables for Claude
|
||||||
|
# claude_env: |
|
||||||
|
# NODE_ENV: test
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type BaseContext = {
|
|||||||
useStickyComment: boolean;
|
useStickyComment: boolean;
|
||||||
additionalPermissions: Map<string, string>;
|
additionalPermissions: Map<string, string>;
|
||||||
useCommitSigning: boolean;
|
useCommitSigning: boolean;
|
||||||
|
allowedBots: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
process.env.ADDITIONAL_PERMISSIONS ?? "",
|
process.env.ADDITIONAL_PERMISSIONS ?? "",
|
||||||
),
|
),
|
||||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||||
|
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
return appToken;
|
return appToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
`Failed to setup GitHub token: ${error}.\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
|
`Failed to setup GitHub token: ${error}\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,41 @@ export function sanitizeContent(content: string): string {
|
|||||||
content = stripMarkdownLinkTitles(content);
|
content = stripMarkdownLinkTitles(content);
|
||||||
content = stripHiddenAttributes(content);
|
content = stripHiddenAttributes(content);
|
||||||
content = normalizeHtmlEntities(content);
|
content = normalizeHtmlEntities(content);
|
||||||
|
content = redactGitHubTokens(content);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactGitHubTokens(content: string): string {
|
||||||
|
// GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bghp_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub OAuth tokens: gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bgho_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub installation tokens: ghs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bghs_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub refresh tokens: ghr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bghr_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub fine-grained personal access tokens: github_pat_XXXXXXXXXX (up to 255 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bgithub_pat_[A-Za-z0-9_]{11,221}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,42 @@ export async function checkHumanActor(
|
|||||||
|
|
||||||
console.log(`Actor type: ${actorType}`);
|
console.log(`Actor type: ${actorType}`);
|
||||||
|
|
||||||
|
// Check bot permissions if actor is not a User
|
||||||
if (actorType !== "User") {
|
if (actorType !== "User") {
|
||||||
|
const allowedBots = githubContext.inputs.allowedBots;
|
||||||
|
|
||||||
|
// Check if all bots are allowed
|
||||||
|
if (allowedBots.trim() === "*") {
|
||||||
|
console.log(
|
||||||
|
`All bots are allowed, skipping human actor check for: ${githubContext.actor}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse allowed bots list
|
||||||
|
const allowedBotsList = allowedBots
|
||||||
|
.split(",")
|
||||||
|
.map((bot) =>
|
||||||
|
bot
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\[bot\]$/, ""),
|
||||||
|
)
|
||||||
|
.filter((bot) => bot.length > 0);
|
||||||
|
|
||||||
|
const botName = githubContext.actor.toLowerCase().replace(/\[bot\]$/, "");
|
||||||
|
|
||||||
|
// Check if specific bot is allowed
|
||||||
|
if (allowedBotsList.includes(botName)) {
|
||||||
|
console.log(
|
||||||
|
`Bot ${botName} is in allowed list, skipping human actor check`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bot not allowed
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Workflow initiated by non-human actor: ${githubContext.actor} (type: ${actorType}).`,
|
`Workflow initiated by non-human actor: ${botName} (type: ${actorType}). Add bot to allowed_bots list or use '*' to allow all bots.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export async function checkWritePermissions(
|
|||||||
try {
|
try {
|
||||||
core.info(`Checking permissions for actor: ${actor}`);
|
core.info(`Checking permissions for actor: ${actor}`);
|
||||||
|
|
||||||
|
// Check if the actor is a GitHub App (bot user)
|
||||||
|
if (actor.endsWith("[bot]")) {
|
||||||
|
core.info(`Actor is a GitHub App: ${actor}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check permissions directly using the permission endpoint
|
// Check permissions directly using the permission endpoint
|
||||||
const response = await octokit.repos.getCollaboratorPermissionLevel({
|
const response = await octokit.repos.getCollaboratorPermissionLevel({
|
||||||
owner: repository.owner,
|
owner: repository.owner,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|||||||
import { GITHUB_API_URL } from "../github/api/config";
|
import { GITHUB_API_URL } from "../github/api/config";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||||
|
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||||
|
|
||||||
// Get repository information from environment variables
|
// Get repository information from environment variables
|
||||||
const REPO_OWNER = process.env.REPO_OWNER;
|
const REPO_OWNER = process.env.REPO_OWNER;
|
||||||
@@ -54,11 +55,13 @@ server.tool(
|
|||||||
const isPullRequestReviewComment =
|
const isPullRequestReviewComment =
|
||||||
eventName === "pull_request_review_comment";
|
eventName === "pull_request_review_comment";
|
||||||
|
|
||||||
|
const sanitizedBody = sanitizeContent(body);
|
||||||
|
|
||||||
const result = await updateClaudeComment(octokit, {
|
const result = await updateClaudeComment(octokit, {
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
commentId,
|
commentId,
|
||||||
body,
|
body: sanitizedBody,
|
||||||
isPullRequestReviewComment,
|
isPullRequestReviewComment,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createOctokit } from "../github/api/client";
|
import { createOctokit } from "../github/api/client";
|
||||||
|
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||||
|
|
||||||
// Get repository and PR information from environment variables
|
// Get repository and PR information from environment variables
|
||||||
const REPO_OWNER = process.env.REPO_OWNER;
|
const REPO_OWNER = process.env.REPO_OWNER;
|
||||||
@@ -41,12 +42,14 @@ server.tool(
|
|||||||
),
|
),
|
||||||
line: z
|
line: z
|
||||||
.number()
|
.number()
|
||||||
|
.nonnegative()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Line number for single-line comments (required if startLine is not provided)",
|
"Line number for single-line comments (required if startLine is not provided)",
|
||||||
),
|
),
|
||||||
startLine: z
|
startLine: z
|
||||||
.number()
|
.number()
|
||||||
|
.nonnegative()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Start line for multi-line comments (use with line parameter for the end line)",
|
"Start line for multi-line comments (use with line parameter for the end line)",
|
||||||
@@ -79,6 +82,9 @@ server.tool(
|
|||||||
|
|
||||||
const octokit = createOctokit(githubToken).rest;
|
const octokit = createOctokit(githubToken).rest;
|
||||||
|
|
||||||
|
// Sanitize the comment body to remove any potential GitHub tokens
|
||||||
|
const sanitizedBody = sanitizeContent(body);
|
||||||
|
|
||||||
// Validate that either line or both startLine and line are provided
|
// Validate that either line or both startLine and line are provided
|
||||||
if (!line && !startLine) {
|
if (!line && !startLine) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -102,7 +108,7 @@ server.tool(
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
pull_number,
|
pull_number,
|
||||||
body,
|
body: sanitizedBody,
|
||||||
path,
|
path,
|
||||||
side: side || "RIGHT",
|
side: side || "RIGHT",
|
||||||
commit_id: commit_id || pr.data.head.sha,
|
commit_id: commit_id || pr.data.head.sha,
|
||||||
|
|||||||
@@ -80,9 +80,8 @@ export const agentMode: Mode = {
|
|||||||
...context.inputs.disallowedTools,
|
...context.inputs.disallowedTools,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Export as INPUT_ prefixed variables for the base action
|
core.exportVariable("ALLOWED_TOOLS", allowedTools.join(","));
|
||||||
core.exportVariable("INPUT_ALLOWED_TOOLS", allowedTools.join(","));
|
core.exportVariable("DISALLOWED_TOOLS", disallowedTools.join(","));
|
||||||
core.exportVariable("INPUT_DISALLOWED_TOOLS", disallowedTools.join(","));
|
|
||||||
|
|
||||||
// Agent mode uses a minimal MCP configuration
|
// Agent mode uses a minimal MCP configuration
|
||||||
// We don't need comment servers or PR-specific tools for automation
|
// We don't need comment servers or PR-specific tools for automation
|
||||||
|
|||||||
@@ -297,9 +297,8 @@ This ensures users get value from the review even before checking individual inl
|
|||||||
...context.inputs.disallowedTools,
|
...context.inputs.disallowedTools,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Export as INPUT_ prefixed variables for the base action
|
core.exportVariable("ALLOWED_TOOLS", allowedTools.join(","));
|
||||||
core.exportVariable("INPUT_ALLOWED_TOOLS", allowedTools.join(","));
|
core.exportVariable("DISALLOWED_TOOLS", disallowedTools.join(","));
|
||||||
core.exportVariable("INPUT_DISALLOWED_TOOLS", disallowedTools.join(","));
|
|
||||||
|
|
||||||
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||||
const mcpConfig = await prepareMcpConfig({
|
const mcpConfig = await prepareMcpConfig({
|
||||||
|
|||||||
96
test/actor.test.ts
Normal file
96
test/actor.test.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { describe, test, expect } from "bun:test";
|
||||||
|
import { checkHumanActor } from "../src/github/validation/actor";
|
||||||
|
import type { Octokit } from "@octokit/rest";
|
||||||
|
import { createMockContext } from "./mockContext";
|
||||||
|
|
||||||
|
function createMockOctokit(userType: string): Octokit {
|
||||||
|
return {
|
||||||
|
users: {
|
||||||
|
getByUsername: async () => ({
|
||||||
|
data: {
|
||||||
|
type: userType,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
} as unknown as Octokit;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("checkHumanActor", () => {
|
||||||
|
test("should pass for human actor", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("User");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "human-user";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should throw error for bot actor when not allowed", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "test-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "";
|
||||||
|
|
||||||
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: test-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should pass for bot actor when all bots allowed", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "test-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "*";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should pass for specific bot when in allowed list", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "dependabot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should pass for specific bot when in allowed list (without [bot])", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "dependabot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot,renovate";
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
checkHumanActor(mockOctokit, context),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should throw error for bot not in allowed list", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "other-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
|
||||||
|
|
||||||
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should throw error for bot not in allowed list (without [bot])", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("Bot");
|
||||||
|
const context = createMockContext();
|
||||||
|
context.actor = "other-bot[bot]";
|
||||||
|
context.inputs.allowedBots = "dependabot,renovate";
|
||||||
|
|
||||||
|
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -37,6 +37,7 @@ describe("prepareMcpConfig", () => {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map(),
|
additionalPermissions: new Map(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const defaultInputs = {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map<string, string>(),
|
additionalPermissions: new Map<string, string>(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRepository = {
|
const defaultRepository = {
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
import { describe, test, expect, beforeEach } from "bun:test";
|
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
import type { GitHubContext } from "../../src/github/context";
|
import type { GitHubContext } from "../../src/github/context";
|
||||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
describe("Agent Mode", () => {
|
describe("Agent Mode", () => {
|
||||||
let mockContext: GitHubContext;
|
let mockContext: GitHubContext;
|
||||||
|
let exportVariableSpy: any;
|
||||||
|
let setOutputSpy: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = createMockAutomationContext({
|
mockContext = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
});
|
});
|
||||||
|
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
exportVariableSpy?.mockClear();
|
||||||
|
setOutputSpy?.mockClear();
|
||||||
|
exportVariableSpy?.mockRestore();
|
||||||
|
setOutputSpy?.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode has correct properties", () => {
|
test("agent mode has correct properties", () => {
|
||||||
@@ -56,4 +70,67 @@ describe("Agent Mode", () => {
|
|||||||
expect(agentMode.shouldTrigger(context)).toBe(false);
|
expect(agentMode.shouldTrigger(context)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prepare method sets up tools environment variables correctly", async () => {
|
||||||
|
// Clear any previous calls before this test
|
||||||
|
exportVariableSpy.mockClear();
|
||||||
|
setOutputSpy.mockClear();
|
||||||
|
|
||||||
|
const contextWithCustomTools = createMockAutomationContext({
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
});
|
||||||
|
contextWithCustomTools.inputs.allowedTools = ["CustomTool1", "CustomTool2"];
|
||||||
|
contextWithCustomTools.inputs.disallowedTools = ["BadTool"];
|
||||||
|
|
||||||
|
const mockOctokit = {} as any;
|
||||||
|
const result = await agentMode.prepare({
|
||||||
|
context: contextWithCustomTools,
|
||||||
|
octokit: mockOctokit,
|
||||||
|
githubToken: "test-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that both ALLOWED_TOOLS and DISALLOWED_TOOLS are set
|
||||||
|
expect(exportVariableSpy).toHaveBeenCalledWith(
|
||||||
|
"ALLOWED_TOOLS",
|
||||||
|
"Edit,MultiEdit,Glob,Grep,LS,Read,Write,CustomTool1,CustomTool2",
|
||||||
|
);
|
||||||
|
expect(exportVariableSpy).toHaveBeenCalledWith(
|
||||||
|
"DISALLOWED_TOOLS",
|
||||||
|
"WebSearch,WebFetch,BadTool",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify MCP config is set
|
||||||
|
expect(setOutputSpy).toHaveBeenCalledWith("mcp_config", expect.any(String));
|
||||||
|
|
||||||
|
// Verify return structure
|
||||||
|
expect(result).toEqual({
|
||||||
|
commentId: undefined,
|
||||||
|
branchInfo: {
|
||||||
|
baseBranch: "",
|
||||||
|
currentBranch: "",
|
||||||
|
claudeBranch: undefined,
|
||||||
|
},
|
||||||
|
mcpConfig: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prepare method creates prompt file with correct content", async () => {
|
||||||
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
});
|
||||||
|
contextWithPrompts.inputs.overridePrompt = "Custom override prompt";
|
||||||
|
contextWithPrompts.inputs.directPrompt =
|
||||||
|
"Direct prompt (should be ignored)";
|
||||||
|
|
||||||
|
const mockOctokit = {} as any;
|
||||||
|
await agentMode.prepare({
|
||||||
|
context: contextWithPrompts,
|
||||||
|
octokit: mockOctokit,
|
||||||
|
githubToken: "test-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: We can't easily test file creation in this unit test,
|
||||||
|
// but we can verify the method completes without errors
|
||||||
|
expect(setOutputSpy).toHaveBeenCalledWith("mcp_config", expect.any(String));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ describe("checkWritePermissions", () => {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map(),
|
additionalPermissions: new Map(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,6 +127,16 @@ describe("checkWritePermissions", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should return true for bot user", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("none");
|
||||||
|
const context = createContext();
|
||||||
|
context.actor = "test-bot[bot]";
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(mockOctokit, context);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("should throw error when permission check fails", async () => {
|
test("should throw error when permission check fails", async () => {
|
||||||
const error = new Error("API error");
|
const error = new Error("API error");
|
||||||
const mockOctokit = {
|
const mockOctokit = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
normalizeHtmlEntities,
|
normalizeHtmlEntities,
|
||||||
sanitizeContent,
|
sanitizeContent,
|
||||||
stripHtmlComments,
|
stripHtmlComments,
|
||||||
|
redactGitHubTokens,
|
||||||
} from "../src/github/utils/sanitizer";
|
} from "../src/github/utils/sanitizer";
|
||||||
|
|
||||||
describe("stripInvisibleCharacters", () => {
|
describe("stripInvisibleCharacters", () => {
|
||||||
@@ -242,6 +243,109 @@ describe("sanitizeContent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("redactGitHubTokens", () => {
|
||||||
|
it("should redact personal access tokens (ghp_)", () => {
|
||||||
|
const token = "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";
|
||||||
|
expect(redactGitHubTokens(`Token: ${token}`)).toBe(
|
||||||
|
"Token: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
expect(redactGitHubTokens(`Here's a token: ${token} in text`)).toBe(
|
||||||
|
"Here's a token: [REDACTED_GITHUB_TOKEN] in text",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact OAuth tokens (gho_)", () => {
|
||||||
|
const token = "gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
||||||
|
expect(redactGitHubTokens(`OAuth: ${token}`)).toBe(
|
||||||
|
"OAuth: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact installation tokens (ghs_)", () => {
|
||||||
|
const token = "ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW";
|
||||||
|
expect(redactGitHubTokens(`Install token: ${token}`)).toBe(
|
||||||
|
"Install token: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact refresh tokens (ghr_)", () => {
|
||||||
|
const token = "ghr_1B4a2e77838347a253e56d7b5253e7d11667";
|
||||||
|
expect(redactGitHubTokens(`Refresh: ${token}`)).toBe(
|
||||||
|
"Refresh: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact fine-grained tokens (github_pat_)", () => {
|
||||||
|
const token =
|
||||||
|
"github_pat_11ABCDEFG0example5of9_2nVwvsylpmOLboQwTPTLewDcE621dQ0AAaBBCCDDEEFFHH";
|
||||||
|
expect(redactGitHubTokens(`Fine-grained: ${token}`)).toBe(
|
||||||
|
"Fine-grained: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tokens in code blocks", () => {
|
||||||
|
const content = `\`\`\`bash
|
||||||
|
export GITHUB_TOKEN=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
|
||||||
|
\`\`\``;
|
||||||
|
const expected = `\`\`\`bash
|
||||||
|
export GITHUB_TOKEN=[REDACTED_GITHUB_TOKEN]
|
||||||
|
\`\`\``;
|
||||||
|
expect(redactGitHubTokens(content)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple tokens in one text", () => {
|
||||||
|
const content =
|
||||||
|
"Token 1: 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 and token 2: gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(
|
||||||
|
"Token 1: [REDACTED_GITHUB_TOKEN] and token 2: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tokens in URLs", () => {
|
||||||
|
const content =
|
||||||
|
"https://api.github.com/user?access_token=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";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(
|
||||||
|
"https://api.github.com/user?access_token=[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not redact partial matches or invalid tokens", () => {
|
||||||
|
const content =
|
||||||
|
"This is not a token: ghp_short or gho_toolong1234567890123456789012345678901234567890";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve normal text", () => {
|
||||||
|
const content = "Normal text with no tokens";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle edge cases", () => {
|
||||||
|
expect(redactGitHubTokens("")).toBe("");
|
||||||
|
expect(redactGitHubTokens("ghp_")).toBe("ghp_");
|
||||||
|
expect(redactGitHubTokens("github_pat_short")).toBe("github_pat_short");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitizeContent with token redaction", () => {
|
||||||
|
it("should redact tokens as part of full sanitization", () => {
|
||||||
|
const content = `
|
||||||
|
<!-- Hidden comment with token: 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 -->
|
||||||
|
Here's some text with a token: gho_16C7e42F292c6912E7710c838347Ae178B4a
|
||||||
|
And invisible chars: test\u200Btoken
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sanitized = sanitizeContent(content);
|
||||||
|
|
||||||
|
expect(sanitized).not.toContain("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");
|
||||||
|
expect(sanitized).not.toContain("gho_16C7e42F292c6912E7710c838347Ae178B4a");
|
||||||
|
expect(sanitized).not.toContain("<!-- Hidden comment");
|
||||||
|
expect(sanitized).not.toContain("\u200B");
|
||||||
|
expect(sanitized).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||||
|
expect(sanitized).toContain("Here's some text with a token:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("stripHtmlComments (legacy)", () => {
|
describe("stripHtmlComments (legacy)", () => {
|
||||||
it("should remove HTML comments", () => {
|
it("should remove HTML comments", () => {
|
||||||
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(
|
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map(),
|
additionalPermissions: new Map(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -74,6 +75,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map(),
|
additionalPermissions: new Map(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
@@ -291,6 +293,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map(),
|
additionalPermissions: new Map(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -325,6 +328,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map(),
|
additionalPermissions: new Map(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -359,6 +363,7 @@ describe("checkContainsTrigger", () => {
|
|||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
additionalPermissions: new Map(),
|
additionalPermissions: new Map(),
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
allowedBots: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user