mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-28 02:42:25 +08:00
Compare commits
8 Commits
v1.0.32
...
ashwin/unr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d090d03da6 | ||
|
|
fe72061e16 | ||
|
|
231bd75b71 | ||
|
|
4126f9d975 | ||
|
|
ba45bb9506 | ||
|
|
0c704179b5 | ||
|
|
f64219702d | ||
|
|
8341a564b0 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
prettier:
|
prettier:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v1
|
- uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
typecheck:
|
typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/claude-review.yml
vendored
2
.github/workflows/claude-review.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
next_version: ${{ steps.next_version.outputs.next_version }}
|
next_version: ${{ steps.next_version.outputs.next_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ jobs:
|
|||||||
# environment: production
|
# environment: production
|
||||||
# steps:
|
# steps:
|
||||||
# - name: Checkout base-action repo
|
# - name: Checkout base-action repo
|
||||||
# uses: actions/checkout@v5
|
# uses: actions/checkout@v6
|
||||||
# with:
|
# with:
|
||||||
# repository: anthropics/claude-code-base-action
|
# repository: anthropics/claude-code-base-action
|
||||||
# token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
# token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||||
|
|||||||
12
action.yml
12
action.yml
@@ -35,6 +35,14 @@ inputs:
|
|||||||
description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)."
|
description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
include_comments_by_actor:
|
||||||
|
description: "Comma-separated list of actor usernames to INCLUDE in comments. Supports wildcards: '*[bot]' matches all bots, 'dependabot[bot]' matches specific bot. Empty (default) includes all actors."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
exclude_comments_by_actor:
|
||||||
|
description: "Comma-separated list of actor usernames to EXCLUDE from comments. Supports wildcards: '*[bot]' matches all bots, 'renovate[bot]' matches specific bot. Empty (default) excludes none. If actor is in both lists, exclusion takes priority."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
# Claude Code configuration
|
# Claude Code configuration
|
||||||
prompt:
|
prompt:
|
||||||
@@ -186,6 +194,8 @@ runs:
|
|||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
||||||
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
|
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
|
||||||
|
INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }}
|
||||||
|
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
|
||||||
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 }}
|
||||||
@@ -213,7 +223,7 @@ runs:
|
|||||||
|
|
||||||
# Install Claude Code if no custom executable is provided
|
# Install Claude Code if no custom executable is provided
|
||||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||||
CLAUDE_CODE_VERSION="2.1.16"
|
CLAUDE_CODE_VERSION="2.1.20"
|
||||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "Installation attempt $attempt..."
|
echo "Installation attempt $attempt..."
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ runs:
|
|||||||
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||||
CLAUDE_CODE_VERSION="2.1.16"
|
CLAUDE_CODE_VERSION="2.1.20"
|
||||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "Installation attempt $attempt..."
|
echo "Installation attempt $attempt..."
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"name": "@anthropic-ai/claude-code-base-action",
|
"name": "@anthropic-ai/claude-code-base-action",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.16",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.16", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-8sG7rvJZ7rc+oj0ZvWMTAtnYYTsh5gP5pCXiG21wYbwHqgEPod/oOIu5DCC/PWhwzN0sAmDbVURgCTDmimYlXw=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.20", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-Q2rJlYC2hEhJRKcOswJrcvm0O6H/uhXkRPAAqbAlFR/jbCWeg6jpyr9iUmVBFUFOBzAWqT2C6KLHiTJ8NySvQg=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.16",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
|
||||||
"shell-quote": "^1.8.3"
|
"shell-quote": "^1.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -7,7 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.16",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.16", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-8sG7rvJZ7rc+oj0ZvWMTAtnYYTsh5gP5pCXiG21wYbwHqgEPod/oOIu5DCC/PWhwzN0sAmDbVURgCTDmimYlXw=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.20", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-Q2rJlYC2hEhJRKcOswJrcvm0O6H/uhXkRPAAqbAlFR/jbCWeg6jpyr9iUmVBFUFOBzAWqT2C6KLHiTJ8NySvQg=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -172,9 +172,14 @@ jobs:
|
|||||||
|
|
||||||
**Important Notes**:
|
**Important Notes**:
|
||||||
|
|
||||||
- The GitHub token must have the `actions: read` permission in your workflow
|
- The GitHub token must have the corresponding permission in your workflow
|
||||||
- If the permission is missing, Claude will warn you and suggest adding it
|
- If the permission is missing, Claude will warn you and suggest adding it
|
||||||
- Currently, only `actions: read` is supported, but the format allows for future extensions
|
- The following additional permissions can be requested beyond the defaults:
|
||||||
|
- `actions: read`
|
||||||
|
- `checks: read`
|
||||||
|
- `discussions: read` or `discussions: write`
|
||||||
|
- `workflows: read` or `workflows: write`
|
||||||
|
- Standard permissions (`contents: write`, `pull_requests: write`, `issues: write`) are always included and do not need to be specified
|
||||||
|
|
||||||
## Custom Environment Variables
|
## Custom Environment Variables
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ For performance, Claude uses shallow clones:
|
|||||||
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
|
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
|
||||||
|
|
||||||
```
|
```
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
depth: 0 # will fetch full repo history
|
depth: 0 # will fetch full repo history
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -513,7 +513,7 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.workflow_run.head_branch }}
|
ref: ${{ github.event.workflow_run.head_branch }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
actions: read # Required for Claude to read CI results on PRs
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.16",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ type BaseContext = {
|
|||||||
allowedNonWriteUsers: string;
|
allowedNonWriteUsers: string;
|
||||||
trackProgress: boolean;
|
trackProgress: boolean;
|
||||||
includeFixLinks: boolean;
|
includeFixLinks: boolean;
|
||||||
|
includeCommentsByActor: string;
|
||||||
|
excludeCommentsByActor: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,6 +158,8 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
|
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
|
||||||
trackProgress: process.env.TRACK_PROGRESS === "true",
|
trackProgress: process.env.TRACK_PROGRESS === "true",
|
||||||
includeFixLinks: process.env.INCLUDE_FIX_LINKS === "true",
|
includeFixLinks: process.env.INCLUDE_FIX_LINKS === "true",
|
||||||
|
includeCommentsByActor: process.env.INCLUDE_COMMENTS_BY_ACTOR ?? "",
|
||||||
|
excludeCommentsByActor: process.env.EXCLUDE_COMMENTS_BY_ACTOR ?? "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import type {
|
|||||||
} from "../types";
|
} from "../types";
|
||||||
import type { CommentWithImages } from "../utils/image-downloader";
|
import type { CommentWithImages } from "../utils/image-downloader";
|
||||||
import { downloadCommentImages } from "../utils/image-downloader";
|
import { downloadCommentImages } from "../utils/image-downloader";
|
||||||
|
import {
|
||||||
|
parseActorFilter,
|
||||||
|
shouldIncludeCommentByActor,
|
||||||
|
} from "../utils/actor-filter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the trigger timestamp from the GitHub webhook payload.
|
* Extracts the trigger timestamp from the GitHub webhook payload.
|
||||||
@@ -166,6 +170,35 @@ export function isBodySafeToUse(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters comments by actor username based on include/exclude patterns
|
||||||
|
* @param comments - Array of comments to filter
|
||||||
|
* @param includeActors - Comma-separated actors to include
|
||||||
|
* @param excludeActors - Comma-separated actors to exclude
|
||||||
|
* @returns Filtered array of comments
|
||||||
|
*/
|
||||||
|
export function filterCommentsByActor<T extends { author: { login: string } }>(
|
||||||
|
comments: T[],
|
||||||
|
includeActors: string = "",
|
||||||
|
excludeActors: string = "",
|
||||||
|
): T[] {
|
||||||
|
const includeParsed = parseActorFilter(includeActors);
|
||||||
|
const excludeParsed = parseActorFilter(excludeActors);
|
||||||
|
|
||||||
|
// No filters = return all
|
||||||
|
if (includeParsed.length === 0 && excludeParsed.length === 0) {
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments.filter((comment) =>
|
||||||
|
shouldIncludeCommentByActor(
|
||||||
|
comment.author.login,
|
||||||
|
includeParsed,
|
||||||
|
excludeParsed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type FetchDataParams = {
|
type FetchDataParams = {
|
||||||
octokits: Octokits;
|
octokits: Octokits;
|
||||||
repository: string;
|
repository: string;
|
||||||
@@ -174,6 +207,8 @@ type FetchDataParams = {
|
|||||||
triggerUsername?: string;
|
triggerUsername?: string;
|
||||||
triggerTime?: string;
|
triggerTime?: string;
|
||||||
originalTitle?: string;
|
originalTitle?: string;
|
||||||
|
includeCommentsByActor?: string;
|
||||||
|
excludeCommentsByActor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GitHubFileWithSHA = GitHubFile & {
|
export type GitHubFileWithSHA = GitHubFile & {
|
||||||
@@ -198,6 +233,8 @@ export async function fetchGitHubData({
|
|||||||
triggerUsername,
|
triggerUsername,
|
||||||
triggerTime,
|
triggerTime,
|
||||||
originalTitle,
|
originalTitle,
|
||||||
|
includeCommentsByActor,
|
||||||
|
excludeCommentsByActor,
|
||||||
}: FetchDataParams): Promise<FetchDataResult> {
|
}: FetchDataParams): Promise<FetchDataResult> {
|
||||||
const [owner, repo] = repository.split("/");
|
const [owner, repo] = repository.split("/");
|
||||||
if (!owner || !repo) {
|
if (!owner || !repo) {
|
||||||
@@ -225,9 +262,13 @@ export async function fetchGitHubData({
|
|||||||
const pullRequest = prResult.repository.pullRequest;
|
const pullRequest = prResult.repository.pullRequest;
|
||||||
contextData = pullRequest;
|
contextData = pullRequest;
|
||||||
changedFiles = pullRequest.files.nodes || [];
|
changedFiles = pullRequest.files.nodes || [];
|
||||||
comments = filterCommentsToTriggerTime(
|
comments = filterCommentsByActor(
|
||||||
|
filterCommentsToTriggerTime(
|
||||||
pullRequest.comments?.nodes || [],
|
pullRequest.comments?.nodes || [],
|
||||||
triggerTime,
|
triggerTime,
|
||||||
|
),
|
||||||
|
includeCommentsByActor,
|
||||||
|
excludeCommentsByActor,
|
||||||
);
|
);
|
||||||
reviewData = pullRequest.reviews || [];
|
reviewData = pullRequest.reviews || [];
|
||||||
|
|
||||||
@@ -248,9 +289,13 @@ export async function fetchGitHubData({
|
|||||||
|
|
||||||
if (issueResult.repository.issue) {
|
if (issueResult.repository.issue) {
|
||||||
contextData = issueResult.repository.issue;
|
contextData = issueResult.repository.issue;
|
||||||
comments = filterCommentsToTriggerTime(
|
comments = filterCommentsByActor(
|
||||||
|
filterCommentsToTriggerTime(
|
||||||
contextData?.comments?.nodes || [],
|
contextData?.comments?.nodes || [],
|
||||||
triggerTime,
|
triggerTime,
|
||||||
|
),
|
||||||
|
includeCommentsByActor,
|
||||||
|
excludeCommentsByActor,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Successfully fetched issue #${prNumber} data`);
|
console.log(`Successfully fetched issue #${prNumber} data`);
|
||||||
@@ -318,7 +363,27 @@ export async function fetchGitHubData({
|
|||||||
body: r.body,
|
body: r.body,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Filter review comments to trigger time
|
// Filter review comments to trigger time and by actor
|
||||||
|
if (reviewData && reviewData.nodes) {
|
||||||
|
// Filter reviews by actor
|
||||||
|
reviewData.nodes = filterCommentsByActor(
|
||||||
|
reviewData.nodes,
|
||||||
|
includeCommentsByActor,
|
||||||
|
excludeCommentsByActor,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also filter inline review comments within each review
|
||||||
|
reviewData.nodes.forEach((review) => {
|
||||||
|
if (review.comments?.nodes) {
|
||||||
|
review.comments.nodes = filterCommentsByActor(
|
||||||
|
review.comments.nodes,
|
||||||
|
includeCommentsByActor,
|
||||||
|
excludeCommentsByActor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const allReviewComments =
|
const allReviewComments =
|
||||||
reviewData?.nodes?.flatMap((r) => r.comments?.nodes ?? []) ?? [];
|
reviewData?.nodes?.flatMap((r) => r.comments?.nodes ?? []) ?? [];
|
||||||
const filteredReviewComments = filterCommentsToTriggerTime(
|
const filteredReviewComments = filterCommentsToTriggerTime(
|
||||||
|
|||||||
@@ -16,15 +16,60 @@ async function getOidcToken(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
const DEFAULT_PERMISSIONS: Record<string, string> = {
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseAdditionalPermissions():
|
||||||
|
| Record<string, string>
|
||||||
|
| undefined {
|
||||||
|
const raw = process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
if (!raw || !raw.trim()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additional: Record<string, string> = {};
|
||||||
|
for (const line of raw.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const colonIndex = trimmed.indexOf(":");
|
||||||
|
if (colonIndex === -1) continue;
|
||||||
|
const key = trimmed.slice(0, colonIndex).trim();
|
||||||
|
const value = trimmed.slice(colonIndex + 1).trim();
|
||||||
|
if (key && value) {
|
||||||
|
additional[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(additional).length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...DEFAULT_PERMISSIONS, ...additional };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exchangeForAppToken(
|
||||||
|
oidcToken: string,
|
||||||
|
permissions?: Record<string, string>,
|
||||||
|
): Promise<string> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
|
};
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (permissions) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
fetchOptions.body = JSON.stringify({ permissions });
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://api.anthropic.com/api/github/github-app-token-exchange",
|
"https://api.anthropic.com/api/github/github-app-token-exchange",
|
||||||
{
|
fetchOptions,
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -89,9 +134,11 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
||||||
console.log("OIDC token successfully obtained");
|
console.log("OIDC token successfully obtained");
|
||||||
|
|
||||||
|
const permissions = parseAdditionalPermissions();
|
||||||
|
|
||||||
console.log("Exchanging OIDC token for app token...");
|
console.log("Exchanging OIDC token for app token...");
|
||||||
const appToken = await retryWithBackoff(() =>
|
const appToken = await retryWithBackoff(() =>
|
||||||
exchangeForAppToken(oidcToken),
|
exchangeForAppToken(oidcToken, permissions),
|
||||||
);
|
);
|
||||||
console.log("App token successfully obtained");
|
console.log("App token successfully obtained");
|
||||||
|
|
||||||
|
|||||||
65
src/github/utils/actor-filter.ts
Normal file
65
src/github/utils/actor-filter.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Parses actor filter string into array of patterns
|
||||||
|
* @param filterString - Comma-separated actor names (e.g., "user1,user2,*[bot]")
|
||||||
|
* @returns Array of actor patterns
|
||||||
|
*/
|
||||||
|
export function parseActorFilter(filterString: string): string[] {
|
||||||
|
if (!filterString.trim()) return [];
|
||||||
|
return filterString
|
||||||
|
.split(",")
|
||||||
|
.map((actor) => actor.trim())
|
||||||
|
.filter((actor) => actor.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an actor matches a pattern
|
||||||
|
* Supports wildcards: "*[bot]" matches all bots, "dependabot[bot]" matches specific
|
||||||
|
* @param actor - Actor username to check
|
||||||
|
* @param pattern - Pattern to match against
|
||||||
|
* @returns true if actor matches pattern
|
||||||
|
*/
|
||||||
|
export function actorMatchesPattern(actor: string, pattern: string): boolean {
|
||||||
|
// Exact match
|
||||||
|
if (actor === pattern) return true;
|
||||||
|
|
||||||
|
// Wildcard bot pattern: "*[bot]" matches any username ending with [bot]
|
||||||
|
if (pattern === "*[bot]" && actor.endsWith("[bot]")) return true;
|
||||||
|
|
||||||
|
// No match
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a comment should be included based on actor filters
|
||||||
|
* @param actor - Comment author username
|
||||||
|
* @param includeActors - Array of actors to include (empty = include all)
|
||||||
|
* @param excludeActors - Array of actors to exclude (empty = exclude none)
|
||||||
|
* @returns true if comment should be included
|
||||||
|
*/
|
||||||
|
export function shouldIncludeCommentByActor(
|
||||||
|
actor: string,
|
||||||
|
includeActors: string[],
|
||||||
|
excludeActors: string[],
|
||||||
|
): boolean {
|
||||||
|
// Check exclusion first (exclusion takes priority)
|
||||||
|
if (excludeActors.length > 0) {
|
||||||
|
for (const pattern of excludeActors) {
|
||||||
|
if (actorMatchesPattern(actor, pattern)) {
|
||||||
|
return false; // Excluded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check inclusion
|
||||||
|
if (includeActors.length > 0) {
|
||||||
|
for (const pattern of includeActors) {
|
||||||
|
if (actorMatchesPattern(actor, pattern)) {
|
||||||
|
return true; // Explicitly included
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // Not in include list
|
||||||
|
}
|
||||||
|
|
||||||
|
// No filters or passed all checks
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -89,6 +89,8 @@ export const tagMode: Mode = {
|
|||||||
triggerUsername: context.actor,
|
triggerUsername: context.actor,
|
||||||
triggerTime,
|
triggerTime,
|
||||||
originalTitle,
|
originalTitle,
|
||||||
|
includeCommentsByActor: context.inputs.includeCommentsByActor,
|
||||||
|
excludeCommentsByActor: context.inputs.excludeCommentsByActor,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup branch
|
// Setup branch
|
||||||
|
|||||||
172
test/actor-filter.test.ts
Normal file
172
test/actor-filter.test.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
parseActorFilter,
|
||||||
|
actorMatchesPattern,
|
||||||
|
shouldIncludeCommentByActor,
|
||||||
|
} from "../src/github/utils/actor-filter";
|
||||||
|
|
||||||
|
describe("parseActorFilter", () => {
|
||||||
|
test("parses comma-separated actors", () => {
|
||||||
|
expect(parseActorFilter("user1,user2,bot[bot]")).toEqual([
|
||||||
|
"user1",
|
||||||
|
"user2",
|
||||||
|
"bot[bot]",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty string", () => {
|
||||||
|
expect(parseActorFilter("")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles whitespace-only string", () => {
|
||||||
|
expect(parseActorFilter(" ")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trims whitespace", () => {
|
||||||
|
expect(parseActorFilter(" user1 , user2 ")).toEqual(["user1", "user2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters out empty entries", () => {
|
||||||
|
expect(parseActorFilter("user1,,user2")).toEqual(["user1", "user2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles single actor", () => {
|
||||||
|
expect(parseActorFilter("user1")).toEqual(["user1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles wildcard bot pattern", () => {
|
||||||
|
expect(parseActorFilter("*[bot]")).toEqual(["*[bot]"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("actorMatchesPattern", () => {
|
||||||
|
test("matches exact username", () => {
|
||||||
|
expect(actorMatchesPattern("john-doe", "john-doe")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not match different username", () => {
|
||||||
|
expect(actorMatchesPattern("john-doe", "jane-doe")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matches wildcard bot pattern", () => {
|
||||||
|
expect(actorMatchesPattern("dependabot[bot]", "*[bot]")).toBe(true);
|
||||||
|
expect(actorMatchesPattern("renovate[bot]", "*[bot]")).toBe(true);
|
||||||
|
expect(actorMatchesPattern("github-actions[bot]", "*[bot]")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not match non-bot with wildcard", () => {
|
||||||
|
expect(actorMatchesPattern("john-doe", "*[bot]")).toBe(false);
|
||||||
|
expect(actorMatchesPattern("user-bot", "*[bot]")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matches specific bot", () => {
|
||||||
|
expect(actorMatchesPattern("dependabot[bot]", "dependabot[bot]")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(actorMatchesPattern("renovate[bot]", "renovate[bot]")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not match different specific bot", () => {
|
||||||
|
expect(actorMatchesPattern("dependabot[bot]", "renovate[bot]")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("is case sensitive", () => {
|
||||||
|
expect(actorMatchesPattern("User1", "user1")).toBe(false);
|
||||||
|
expect(actorMatchesPattern("user1", "User1")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shouldIncludeCommentByActor", () => {
|
||||||
|
test("includes all when no filters", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("user1", [], [])).toBe(true);
|
||||||
|
expect(shouldIncludeCommentByActor("bot[bot]", [], [])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("excludes when in exclude list", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("bot[bot]", [], ["*[bot]"])).toBe(false);
|
||||||
|
expect(shouldIncludeCommentByActor("user1", [], ["user1"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("includes when not in exclude list", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("user1", [], ["user2"])).toBe(true);
|
||||||
|
expect(shouldIncludeCommentByActor("user1", [], ["*[bot]"])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("includes when in include list", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("user1", ["user1", "user2"], [])).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(shouldIncludeCommentByActor("user2", ["user1", "user2"], [])).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("excludes when not in include list", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("user3", ["user1", "user2"], [])).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exclusion takes priority over inclusion", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("user1", ["user1"], ["user1"])).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shouldIncludeCommentByActor("bot[bot]", ["*[bot]"], ["*[bot]"]),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles wildcard in include list", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("dependabot[bot]", ["*[bot]"], [])).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(shouldIncludeCommentByActor("renovate[bot]", ["*[bot]"], [])).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(shouldIncludeCommentByActor("user1", ["*[bot]"], [])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles wildcard in exclude list", () => {
|
||||||
|
expect(shouldIncludeCommentByActor("dependabot[bot]", [], ["*[bot]"])).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(shouldIncludeCommentByActor("renovate[bot]", [], ["*[bot]"])).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(shouldIncludeCommentByActor("user1", [], ["*[bot]"])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles mixed include and exclude lists", () => {
|
||||||
|
// Include user1 and user2, but exclude user2
|
||||||
|
expect(
|
||||||
|
shouldIncludeCommentByActor("user1", ["user1", "user2"], ["user2"]),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldIncludeCommentByActor("user2", ["user1", "user2"], ["user2"]),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldIncludeCommentByActor("user3", ["user1", "user2"], ["user2"]),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles complex bot filtering", () => {
|
||||||
|
// Include all bots but exclude dependabot
|
||||||
|
expect(
|
||||||
|
shouldIncludeCommentByActor(
|
||||||
|
"renovate[bot]",
|
||||||
|
["*[bot]"],
|
||||||
|
["dependabot[bot]"],
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldIncludeCommentByActor(
|
||||||
|
"dependabot[bot]",
|
||||||
|
["*[bot]"],
|
||||||
|
["dependabot[bot]"],
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldIncludeCommentByActor("user1", ["*[bot]"], ["dependabot[bot]"]),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it, jest } from "bun:test";
|
import { describe, expect, it, jest, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
extractTriggerTimestamp,
|
extractTriggerTimestamp,
|
||||||
extractOriginalTitle,
|
extractOriginalTitle,
|
||||||
@@ -1100,3 +1100,101 @@ describe("fetchGitHubData integration with time filtering", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("filterCommentsByActor", () => {
|
||||||
|
test("filters out excluded actors", () => {
|
||||||
|
const comments = [
|
||||||
|
{ author: { login: "user1" }, body: "comment1" },
|
||||||
|
{ author: { login: "bot[bot]" }, body: "comment2" },
|
||||||
|
{ author: { login: "user2" }, body: "comment3" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { filterCommentsByActor } = require("../src/github/data/fetcher");
|
||||||
|
const filtered = filterCommentsByActor(comments, "", "*[bot]");
|
||||||
|
expect(filtered).toHaveLength(2);
|
||||||
|
expect(filtered.map((c: any) => c.author.login)).toEqual([
|
||||||
|
"user1",
|
||||||
|
"user2",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("includes only specified actors", () => {
|
||||||
|
const comments = [
|
||||||
|
{ author: { login: "user1" }, body: "comment1" },
|
||||||
|
{ author: { login: "user2" }, body: "comment2" },
|
||||||
|
{ author: { login: "user3" }, body: "comment3" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { filterCommentsByActor } = require("../src/github/data/fetcher");
|
||||||
|
const filtered = filterCommentsByActor(comments, "user1,user2", "");
|
||||||
|
expect(filtered).toHaveLength(2);
|
||||||
|
expect(filtered.map((c: any) => c.author.login)).toEqual([
|
||||||
|
"user1",
|
||||||
|
"user2",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns all when no filters", () => {
|
||||||
|
const comments = [
|
||||||
|
{ author: { login: "user1" }, body: "comment1" },
|
||||||
|
{ author: { login: "user2" }, body: "comment2" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { filterCommentsByActor } = require("../src/github/data/fetcher");
|
||||||
|
const filtered = filterCommentsByActor(comments, "", "");
|
||||||
|
expect(filtered).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exclusion takes priority", () => {
|
||||||
|
const comments = [
|
||||||
|
{ author: { login: "user1" }, body: "comment1" },
|
||||||
|
{ author: { login: "user2" }, body: "comment2" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { filterCommentsByActor } = require("../src/github/data/fetcher");
|
||||||
|
const filtered = filterCommentsByActor(comments, "user1,user2", "user1");
|
||||||
|
expect(filtered).toHaveLength(1);
|
||||||
|
expect(filtered[0].author.login).toBe("user2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters multiple bot types", () => {
|
||||||
|
const comments = [
|
||||||
|
{ author: { login: "user1" }, body: "comment1" },
|
||||||
|
{ author: { login: "dependabot[bot]" }, body: "comment2" },
|
||||||
|
{ author: { login: "renovate[bot]" }, body: "comment3" },
|
||||||
|
{ author: { login: "user2" }, body: "comment4" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { filterCommentsByActor } = require("../src/github/data/fetcher");
|
||||||
|
const filtered = filterCommentsByActor(comments, "", "*[bot]");
|
||||||
|
expect(filtered).toHaveLength(2);
|
||||||
|
expect(filtered.map((c: any) => c.author.login)).toEqual([
|
||||||
|
"user1",
|
||||||
|
"user2",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters specific bot only", () => {
|
||||||
|
const comments = [
|
||||||
|
{ author: { login: "dependabot[bot]" }, body: "comment1" },
|
||||||
|
{ author: { login: "renovate[bot]" }, body: "comment2" },
|
||||||
|
{ author: { login: "user1" }, body: "comment3" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { filterCommentsByActor } = require("../src/github/data/fetcher");
|
||||||
|
const filtered = filterCommentsByActor(comments, "", "dependabot[bot]");
|
||||||
|
expect(filtered).toHaveLength(2);
|
||||||
|
expect(filtered.map((c: any) => c.author.login)).toEqual([
|
||||||
|
"renovate[bot]",
|
||||||
|
"user1",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty comment array", () => {
|
||||||
|
const comments: any[] = [];
|
||||||
|
|
||||||
|
const { filterCommentsByActor } = require("../src/github/data/fetcher");
|
||||||
|
const filtered = filterCommentsByActor(comments, "user1", "");
|
||||||
|
expect(filtered).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ describe("prepareMcpConfig", () => {
|
|||||||
allowedNonWriteUsers: "",
|
allowedNonWriteUsers: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
includeFixLinks: true,
|
includeFixLinks: true,
|
||||||
|
includeCommentsByActor: "",
|
||||||
|
excludeCommentsByActor: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ const defaultInputs = {
|
|||||||
allowedNonWriteUsers: "",
|
allowedNonWriteUsers: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
includeFixLinks: true,
|
includeFixLinks: true,
|
||||||
|
includeCommentsByActor: "",
|
||||||
|
excludeCommentsByActor: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRepository = {
|
const defaultRepository = {
|
||||||
@@ -55,7 +57,12 @@ export const createMockContext = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mergedInputs = overrides.inputs
|
const mergedInputs = overrides.inputs
|
||||||
? { ...defaultInputs, ...overrides.inputs }
|
? {
|
||||||
|
...defaultInputs,
|
||||||
|
...overrides.inputs,
|
||||||
|
includeCommentsByActor: overrides.inputs.includeCommentsByActor ?? "",
|
||||||
|
excludeCommentsByActor: overrides.inputs.excludeCommentsByActor ?? "",
|
||||||
|
}
|
||||||
: defaultInputs;
|
: defaultInputs;
|
||||||
|
|
||||||
return { ...baseContext, ...overrides, inputs: mergedInputs };
|
return { ...baseContext, ...overrides, inputs: mergedInputs };
|
||||||
@@ -79,7 +86,12 @@ export const createMockAutomationContext = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mergedInputs = overrides.inputs
|
const mergedInputs = overrides.inputs
|
||||||
? { ...defaultInputs, ...overrides.inputs }
|
? {
|
||||||
|
...defaultInputs,
|
||||||
|
...overrides.inputs,
|
||||||
|
includeCommentsByActor: overrides.inputs.includeCommentsByActor ?? "",
|
||||||
|
excludeCommentsByActor: overrides.inputs.excludeCommentsByActor ?? "",
|
||||||
|
}
|
||||||
: { ...defaultInputs };
|
: { ...defaultInputs };
|
||||||
|
|
||||||
return { ...baseContext, ...overrides, inputs: mergedInputs };
|
return { ...baseContext, ...overrides, inputs: mergedInputs };
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ describe("detectMode with enhanced routing", () => {
|
|||||||
allowedNonWriteUsers: "",
|
allowedNonWriteUsers: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
includeFixLinks: true,
|
includeFixLinks: true,
|
||||||
|
includeCommentsByActor: "",
|
||||||
|
excludeCommentsByActor: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
97
test/parse-permissions.test.ts
Normal file
97
test/parse-permissions.test.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { parseAdditionalPermissions } from "../src/github/token";
|
||||||
|
|
||||||
|
describe("parseAdditionalPermissions", () => {
|
||||||
|
let originalEnv: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv === undefined) {
|
||||||
|
delete process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
} else {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = originalEnv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when env var is not set", () => {
|
||||||
|
delete process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
expect(parseAdditionalPermissions()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when env var is empty string", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "";
|
||||||
|
expect(parseAdditionalPermissions()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when env var is only whitespace", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = " \n \n ";
|
||||||
|
expect(parseAdditionalPermissions()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses single permission and merges with defaults", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "actions: read";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses multiple permissions", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "actions: read\nworkflows: write";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
workflows: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("additional permissions can override defaults", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "contents: read";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "read",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles extra whitespace around keys and values", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = " actions : read ";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips empty lines", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS =
|
||||||
|
"actions: read\n\n\nworkflows: write\n\n";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
workflows: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips lines without colons", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS =
|
||||||
|
"actions: read\ninvalid line\nworkflows: write";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
workflows: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -75,6 +75,8 @@ describe("checkWritePermissions", () => {
|
|||||||
allowedNonWriteUsers: "",
|
allowedNonWriteUsers: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
includeFixLinks: true,
|
includeFixLinks: true,
|
||||||
|
includeCommentsByActor: "",
|
||||||
|
excludeCommentsByActor: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user