Files
claude-code-action/test/permissions.test.ts
Ashwin Bhat 23fae74fdb Add GitHub Actions MCP server for viewing workflow results (#231)
* actions server

* tmp

* Replace view_actions_results with additional_permissions input

- Changed input from boolean view_actions_results to a more flexible additional_permissions format
- Uses newline-separated colon format similar to claude_env (e.g., "actions: read")
- Maintains permission checking to warn users when their token lacks required permissions
- Updated all tests to use the new format

This allows for future extensibility while currently supporting only "actions: read" permission.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update GitHub Actions MCP server with RUNNER_TEMP and status filtering

- Use RUNNER_TEMP environment variable for log storage directory (defaults to /tmp)
- Add status parameter to get_ci_status tool to filter workflow runs
- Supported statuses: completed, action_required, cancelled, failure, neutral, skipped, stale, success, timed_out, in_progress, queued, requested, waiting, pending
- Pass RUNNER_TEMP from install-mcp-server.ts to the MCP server environment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add GitHub Actions MCP tools to allowed tools when actions:read is granted

- Automatically include github_ci MCP server tools in allowed tools list when actions:read permission is granted
- Added mcp__github_ci__get_ci_status, mcp__github_ci__get_workflow_run_details, mcp__github_ci__download_job_log
- Simplified permission checking to avoid duplicate parsing logic
- Added tests for the new functionality

This ensures Claude can use the Actions tools when the server is enabled.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Refactor additional permissions parsing to parseGitHubContext

- Moved additional permissions parsing from individual functions to centralized parseGitHubContext
- Added parseAdditionalPermissions function to handle newline-separated colon format
- Removed redundant additionalPermissions parameter from prepareMcpConfig
- Updated tests to use permissions from context instead of passing as parameter
- Added comprehensive tests for parseAdditionalPermissions function

This centralizes all input parsing logic in one place for better maintainability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove unnecessary hasActionsReadPermission parameter from createPrompt

- Removed hasActionsReadPermission parameter since createPrompt has access to context
- Calculate hasActionsReadPermission directly from context.inputs.additionalPermissions inside createPrompt
- Simplified prepare.ts by removing intermediate permission check

This completes the refactoring to centralize all permission handling through the context object.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: Add documentation for additional_permissions feature

- Document the new additional_permissions input that replaces view_actions_results
- Add dedicated section explaining CI/CD integration with actions:read permission
- Include example workflow showing how to grant GitHub token permissions
- Update main workflow example to show optional additional_permissions usage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* roadmap

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-03 18:58:02 -07:00

167 lines
4.7 KiB
TypeScript

import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test";
import * as core from "@actions/core";
import { checkWritePermissions } from "../src/github/validation/permissions";
import type { ParsedGitHubContext } from "../src/github/context";
describe("checkWritePermissions", () => {
let coreInfoSpy: any;
let coreWarningSpy: any;
let coreErrorSpy: any;
beforeEach(() => {
// Spy on core methods
coreInfoSpy = spyOn(core, "info").mockImplementation(() => {});
coreWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
coreErrorSpy = spyOn(core, "error").mockImplementation(() => {});
});
afterEach(() => {
coreInfoSpy.mockRestore();
coreWarningSpy.mockRestore();
coreErrorSpy.mockRestore();
});
const createMockOctokit = (permission: string) => {
return {
repos: {
getCollaboratorPermissionLevel: async () => ({
data: { permission },
}),
},
} as any;
};
const createContext = (): ParsedGitHubContext => ({
runId: "1234567890",
eventName: "issue_comment",
eventAction: "created",
repository: {
full_name: "test-owner/test-repo",
owner: "test-owner",
repo: "test-repo",
},
actor: "test-user",
payload: {
action: "created",
issue: {
number: 1,
title: "Test Issue",
body: "Test body",
user: { login: "test-user" },
},
comment: {
id: 123,
body: "@claude test",
user: { login: "test-user" },
html_url:
"https://github.com/test-owner/test-repo/issues/1#issuecomment-123",
},
} as any,
entityNumber: 1,
isPR: false,
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
directPrompt: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
},
});
test("should return true for admin permissions", async () => {
const mockOctokit = createMockOctokit("admin");
const context = createContext();
const result = await checkWritePermissions(mockOctokit, context);
expect(result).toBe(true);
expect(coreInfoSpy).toHaveBeenCalledWith(
"Checking permissions for actor: test-user",
);
expect(coreInfoSpy).toHaveBeenCalledWith(
"Permission level retrieved: admin",
);
expect(coreInfoSpy).toHaveBeenCalledWith("Actor has write access: admin");
});
test("should return true for write permissions", async () => {
const mockOctokit = createMockOctokit("write");
const context = createContext();
const result = await checkWritePermissions(mockOctokit, context);
expect(result).toBe(true);
expect(coreInfoSpy).toHaveBeenCalledWith("Actor has write access: write");
});
test("should return false for read permissions", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(mockOctokit, context);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: read",
);
});
test("should return false for none permissions", async () => {
const mockOctokit = createMockOctokit("none");
const context = createContext();
const result = await checkWritePermissions(mockOctokit, context);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: none",
);
});
test("should throw error when permission check fails", async () => {
const error = new Error("API error");
const mockOctokit = {
repos: {
getCollaboratorPermissionLevel: async () => {
throw error;
},
},
} as any;
const context = createContext();
await expect(checkWritePermissions(mockOctokit, context)).rejects.toThrow(
"Failed to check permissions for test-user: Error: API error",
);
expect(coreErrorSpy).toHaveBeenCalledWith(
"Failed to check permissions: Error: API error",
);
});
test("should call API with correct parameters", async () => {
let capturedParams: any;
const mockOctokit = {
repos: {
getCollaboratorPermissionLevel: async (params: any) => {
capturedParams = params;
return { data: { permission: "write" } };
},
},
} as any;
const context = createContext();
await checkWritePermissions(mockOctokit, context);
expect(capturedParams).toEqual({
owner: "test-owner",
repo: "test-repo",
username: "test-user",
});
});
});