mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 23:54:13 +08:00
Initial commit
This commit is contained in:
31
src/github/validation/actor.ts
Normal file
31
src/github/validation/actor.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Check if the action trigger is from a human actor
|
||||
* Prevents automated tools or bots from triggering Claude
|
||||
*/
|
||||
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
|
||||
export async function checkHumanActor(
|
||||
octokit: Octokit,
|
||||
githubContext: ParsedGitHubContext,
|
||||
) {
|
||||
// Fetch user information from GitHub API
|
||||
const { data: userData } = await octokit.users.getByUsername({
|
||||
username: githubContext.actor,
|
||||
});
|
||||
|
||||
const actorType = userData.type;
|
||||
|
||||
console.log(`Actor type: ${actorType}`);
|
||||
|
||||
if (actorType !== "User") {
|
||||
throw new Error(
|
||||
`Workflow initiated by non-human actor: ${githubContext.actor} (type: ${actorType}).`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Verified human actor: ${githubContext.actor}`);
|
||||
}
|
||||
41
src/github/validation/permissions.ts
Normal file
41
src/github/validation/permissions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as core from "@actions/core";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
|
||||
/**
|
||||
* Check if the actor has write permissions to the repository
|
||||
* @param octokit - The Octokit REST client
|
||||
* @param context - The GitHub context
|
||||
* @returns true if the actor has write permissions, false otherwise
|
||||
*/
|
||||
export async function checkWritePermissions(
|
||||
octokit: Octokit,
|
||||
context: ParsedGitHubContext,
|
||||
): Promise<boolean> {
|
||||
const { repository, actor } = context;
|
||||
|
||||
try {
|
||||
core.info(`Checking permissions for actor: ${actor}`);
|
||||
|
||||
// Check permissions directly using the permission endpoint
|
||||
const response = await octokit.repos.getCollaboratorPermissionLevel({
|
||||
owner: repository.owner,
|
||||
repo: repository.repo,
|
||||
username: actor,
|
||||
});
|
||||
|
||||
const permissionLevel = response.data.permission;
|
||||
core.info(`Permission level retrieved: ${permissionLevel}`);
|
||||
|
||||
if (permissionLevel === "admin" || permissionLevel === "write") {
|
||||
core.info(`Actor has write access: ${permissionLevel}`);
|
||||
return true;
|
||||
} else {
|
||||
core.warning(`Actor has insufficient permissions: ${permissionLevel}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
core.error(`Failed to check permissions: ${error}`);
|
||||
throw new Error(`Failed to check permissions for ${actor}: ${error}`);
|
||||
}
|
||||
}
|
||||
137
src/github/validation/trigger.ts
Normal file
137
src/github/validation/trigger.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import {
|
||||
isIssuesEvent,
|
||||
isIssueCommentEvent,
|
||||
isPullRequestEvent,
|
||||
isPullRequestReviewEvent,
|
||||
isPullRequestReviewCommentEvent,
|
||||
} from "../context";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
|
||||
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
||||
const {
|
||||
inputs: { assigneeTrigger, triggerPhrase, directPrompt },
|
||||
} = context;
|
||||
|
||||
// If direct prompt is provided, always trigger
|
||||
if (directPrompt) {
|
||||
console.log(`Direct prompt provided, triggering action`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for assignee trigger
|
||||
if (isIssuesEvent(context) && context.eventAction === "assigned") {
|
||||
// Remove @ symbol from assignee_trigger if present
|
||||
let triggerUser = assigneeTrigger.replace(/^@/, "");
|
||||
const assigneeUsername = context.payload.issue.assignee?.login || "";
|
||||
|
||||
if (triggerUser && assigneeUsername === triggerUser) {
|
||||
console.log(`Issue assigned to trigger user '${triggerUser}'`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for issue body and title trigger on issue creation
|
||||
if (isIssuesEvent(context) && context.eventAction === "opened") {
|
||||
const issueBody = context.payload.issue.body || "";
|
||||
const issueTitle = context.payload.issue.title || "";
|
||||
// Check for exact match with word boundaries or punctuation
|
||||
const regex = new RegExp(
|
||||
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
|
||||
);
|
||||
|
||||
// Check in body
|
||||
if (regex.test(issueBody)) {
|
||||
console.log(
|
||||
`Issue body contains exact trigger phrase '${triggerPhrase}'`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check in title
|
||||
if (regex.test(issueTitle)) {
|
||||
console.log(
|
||||
`Issue title contains exact trigger phrase '${triggerPhrase}'`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pull request body and title trigger
|
||||
if (isPullRequestEvent(context)) {
|
||||
const prBody = context.payload.pull_request.body || "";
|
||||
const prTitle = context.payload.pull_request.title || "";
|
||||
// Check for exact match with word boundaries or punctuation
|
||||
const regex = new RegExp(
|
||||
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
|
||||
);
|
||||
|
||||
// Check in body
|
||||
if (regex.test(prBody)) {
|
||||
console.log(
|
||||
`Pull request body contains exact trigger phrase '${triggerPhrase}'`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check in title
|
||||
if (regex.test(prTitle)) {
|
||||
console.log(
|
||||
`Pull request title contains exact trigger phrase '${triggerPhrase}'`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pull request review body trigger
|
||||
if (
|
||||
isPullRequestReviewEvent(context) &&
|
||||
(context.eventAction === "submitted" || context.eventAction === "edited")
|
||||
) {
|
||||
const reviewBody = context.payload.review.body || "";
|
||||
// Check for exact match with word boundaries or punctuation
|
||||
const regex = new RegExp(
|
||||
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
|
||||
);
|
||||
if (regex.test(reviewBody)) {
|
||||
console.log(
|
||||
`Pull request review contains exact trigger phrase '${triggerPhrase}'`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for comment trigger
|
||||
if (
|
||||
isIssueCommentEvent(context) ||
|
||||
isPullRequestReviewCommentEvent(context)
|
||||
) {
|
||||
const commentBody = isIssueCommentEvent(context)
|
||||
? context.payload.comment.body
|
||||
: context.payload.comment.body;
|
||||
// Check for exact match with word boundaries or punctuation
|
||||
const regex = new RegExp(
|
||||
`(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`,
|
||||
);
|
||||
if (regex.test(commentBody)) {
|
||||
console.log(`Comment contains exact trigger phrase '${triggerPhrase}'`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`No trigger was met for ${triggerPhrase}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function escapeRegExp(string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export async function checkTriggerAction(context: ParsedGitHubContext) {
|
||||
const containsTrigger = checkContainsTrigger(context);
|
||||
core.setOutput("contains_trigger", containsTrigger.toString());
|
||||
return containsTrigger;
|
||||
}
|
||||
Reference in New Issue
Block a user