Compare commits

...
32 Commits
Author SHA1 Message Date
GitHub Actions 3b8197d3d4 chore: bump Claude Code to 2.1.274 and Agent SDK to 0.3.274 2026-09-17 00:12:40 +00:00
GitHub Actions 7b0b255830 chore: bump Claude Code to 2.1.273 and Agent SDK to 0.3.273 2026-09-15 20:23:44 +00:00
GitHub Actions bf38e86e58 chore: bump Claude Code to 2.1.272 and Agent SDK to 0.3.272 2026-09-15 00:43:27 +00:00
GitHub Actions 51db78a4b8 chore: bump Claude Code to 2.1.271 and Agent SDK to 0.3.271 2026-09-14 22:13:27 +00:00
GitHub Actions 9cdae7f0d9 chore: bump Claude Code to 2.1.270 and Agent SDK to 0.3.270 2026-09-12 19:46:49 +00:00
GitHub Actions 56cf60fde4 chore: bump Claude Code to 2.1.269 and Agent SDK to 0.3.269 2026-09-11 19:18:51 +00:00
GitHub Actions 0a8d3c9443 chore: bump Claude Code to 2.1.268 and Agent SDK to 0.3.268 2026-09-10 20:39:16 +00:00
GitHub Actions 19dda84776 chore: bump Claude Code to 2.1.267 and Agent SDK to 0.3.267 2026-09-09 20:11:01 +00:00
GitHub Actions 5ccc3a35a6 chore: bump Claude Code to 2.1.266 and Agent SDK to 0.3.266 2026-09-08 23:56:12 +00:00
GitHub Actions 0d0e0876d3 chore: bump Claude Code to 2.1.265 and Agent SDK to 0.3.265 2026-09-08 20:38:21 +00:00
GitHub Actions 9c5ddab2e6 chore: bump Claude Code to 2.1.263 and Agent SDK to 0.3.263 2026-09-06 02:55:18 +00:00
GitHub Actions d75b94d5ad chore: bump Claude Code to 2.1.261 and Agent SDK to 0.3.261 2026-09-04 19:58:53 +00:00
GitHub Actions ef8bb1e43b chore: bump Claude Code to 2.1.260 and Agent SDK to 0.3.260 2026-09-03 23:48:49 +00:00
GitHub Actions fa2b2666b7 chore: bump Claude Code to 2.1.259 and Agent SDK to 0.3.259 2026-09-02 22:34:41 +00:00
GitHub Actions 8251c103ac chore: bump Claude Code to 2.1.258 and Agent SDK to 0.3.258 2026-09-01 22:33:40 +00:00
GitHub Actions 781d62e9d5 chore: bump Claude Code to 2.1.257 and Agent SDK to 0.3.257 2026-09-01 17:54:28 +00:00
GitHub Actions 833fb0f8c9 chore: bump Claude Code to 2.1.252 and Agent SDK to 0.3.252 2026-08-31 19:47:59 +00:00
GitHub Actions a874e9ecd7 chore: bump Claude Code to 2.1.251 and Agent SDK to 0.3.251 2026-08-28 18:20:42 +00:00
GitHub Actions a60f3e1db3 chore: bump Claude Code to 2.1.250 and Agent SDK to 0.3.250 2026-08-28 00:50:11 +00:00
GitHub Actions e8c2d7c16c chore: bump Claude Code to 2.1.248 and Agent SDK to 0.3.248 2026-08-27 22:12:44 +00:00
GitHub Actions 70fec18385 chore: bump Claude Code to 2.1.247 and Agent SDK to 0.3.247 2026-08-27 00:09:24 +00:00
GitHub Actions 1f291e1cfe chore: bump Claude Code to 2.1.246 and Agent SDK to 0.3.246 2026-08-25 22:32:51 +00:00
Muhammad Abdullah khanandGitHub 76ac41a83e fix: encode branch names in GitHub links (#1713)
* fix: encode branch names in GitHub links

* style: format branch URL helper
2026-08-25 11:02:49 -07:00
Jeremy SchoemakerandGitHub 8ef9699156 fix: bound download_job_log against a stalled log fetch (#1719)
The download_job_log MCP tool called
client.actions.downloadJobLogsForWorkflowRun() with no timeout and no
AbortController. @octokit/rest@21 runs on Node's native fetch, which has
no default timeout, and Octokit only cancels a request when the caller
passes request.signal. If the log blob fetch stalls, that await never
resolves and never rejects.

This tool is always enabled in tag mode (src/modes/tag/index.ts), so a
"fix the failing CI" run that calls get_ci_status -> get_workflow_run_details
-> download_job_log can hang on this one await with nothing to recover it.
It's headless, so the run only ends when the Actions job-level
timeout-minutes kills it, burning the whole job budget with the tracking
comment stuck at "Claude Code is working...".

Sibling fetch in src/github/utils/image-downloader.ts (fetchImage) already
got this treatment in #1625 via a timeout-driven AbortController. Same
shape of call: fetch a GitHub-hosted resource by ID from untrusted PR/CI
content. This mirrors that fix for github-actions-server.ts.

Extracted the download+write logic into an exported downloadJobLog()
function (with an injectable timeoutMs) so the timeout path is directly
testable, and guarded the module's entrypoint side effects with
import.meta.main, matching the pattern already used by the other
entrypoints in src/entrypoints/.
2026-08-25 08:46:02 -07:00
Yauheni PapovichandGitHub 791545dab1 fix: allow parentheses in valid branch names (#1710)
Accept parentheses while preserving existing branch-name security checks.

Add regression coverage for scoped branch names.

Refs anthropics/claude-code-action#1709
2026-08-25 08:45:50 -07:00
2d7a787fbd fix: use paths in delete_files prompt example (#1702)
* fix: use paths param in delete_files prompt example

The tag-mode prompt told the model to call delete_files with
"files", but the MCP tool schema and handler expect "paths".

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: prove delete_files prompt against the live MCP schema

The old {files} payload is rejected by the same Zod shape the
tool registers; the generated prompt example now parses cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: cover delete_files schema edges and the sibling commit_files tool

Confirm the old files-only payload still fails, types and required
fields are enforced, and commit_files was not inverted by the fix.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: RESILIENCE Agentic Solutions <286555414+WeAreResilience@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 08:45:33 -07:00
Mohammed AlkindiandGitHub b58c16b325 chore: add .gitattributes to normalize line endings (#1708)
On Windows, git's default core.autocrlf=true checks out CRLF, and the contributor commands in CONTRIBUTING.md then fail: bun run format:check reports 191 files because Prettier defaults to endOfLine "lf", and three tests that match LF-terminated content fail.

All tracked blobs are already LF, so this changes checkout behavior only and produces no renormalization diff. Every CI job runs on ubuntu-latest, where it is a no-op.
2026-08-25 08:45:13 -07:00
GitHub Actions 16b3b310c3 chore: bump Claude Code to 2.1.245 and Agent SDK to 0.3.245 2026-08-25 05:14:05 +00:00
GitHub Actions 6bcfb8263a chore: bump Claude Code to 2.1.241 and Agent SDK to 0.3.241 2026-08-25 03:43:35 +00:00
GitHub Actions b62c7454dc chore: bump Claude Code to 2.1.241 and Agent SDK to 0.3.243 2026-08-25 03:38:24 +00:00
GitHub Actions e5ad3c7725 chore: bump Claude Code to 2.1.243 and Agent SDK to 0.3.243 2026-08-24 23:41:35 +00:00
GitHub Actions c81e3bc69d chore: bump Claude Code to 2.1.241 and Agent SDK to 0.3.241 2026-08-23 00:53:06 +00:00
21 changed files with 419 additions and 79 deletions
+3
View File
@@ -0,0 +1,3 @@
# Keep the working tree LF on every platform: Prettier defaults to
# endOfLine "lf" and several tests match LF-terminated content.
* text=auto eol=lf
+1 -1
View File
@@ -147,7 +147,7 @@ runs:
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
run: |
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
CLAUDE_CODE_VERSION="2.1.240"
CLAUDE_CODE_VERSION="2.1.274"
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
for attempt in 1 2 3; do
echo "Installation attempt $attempt..."
+10 -10
View File
@@ -6,7 +6,7 @@
"name": "@anthropic-ai/claude-code-base-action",
"dependencies": {
"@actions/core": "^1.10.1",
"@anthropic-ai/claude-agent-sdk": "^0.3.240",
"@anthropic-ai/claude-agent-sdk": "^0.3.274",
"shell-quote": "^1.8.4",
},
"devDependencies": {
@@ -27,23 +27,23 @@
"@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.3.240", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.240", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.240", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.240", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.240" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-rt/n4rr/Iqem0JtDdL7DdzDnMEei652sS00wVwZ3/0btFt9ah5/iKPKZi8udJ2fjiBwJk6spz9vUQrm6LeyXsA=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.274", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.274", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.274", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.274", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.274" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-kFmWMsh/BEd4jKkOxUeihr0xMkaMdOxONhNNkZ2pGmP0ElHkArvFvuZxqJTPvREnhCsPZByv5f0EzyAb/hkRZw=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.240", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iCGku/IOMvvwSfdFQXnkX2Txzo6nNzz/JoJ2tvmPSnA2NvEEKbSL2P7/WCGSo8asKe1m/6S6EDhYbiagyhc5VQ=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.274", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B0uAdUIbUhWuybOT5FkhbdsALGNduyaHYo+bEggKBSQS3RFRjgNbX/ah1+IwkmTn4UKDN7AX86DkTEjpj6TSWQ=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.240", "", { "os": "darwin", "cpu": "x64" }, "sha512-rA5uZtNGbpxe2+ghzBCZvB30Q4tcFduxkP1qMjI4jOFNq5M4b0O173MaQj+g8D9o0clKbAIEx3tq8pSrtEXotA=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.274", "", { "os": "darwin", "cpu": "x64" }, "sha512-l5pp3Z+z2mti0H3y1O0MRbTg/r+MDm0hly4WpoKmZyIZSOrDQR7jhCqS6Ihj3Vvd7CMP9NZ8t6F0oXVBDCM6YQ=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.240", "", { "os": "linux", "cpu": "arm64" }, "sha512-cvfxos8HdvVlHO9vKpt8i5cC87a5EuLFBtzjtr+ycczipp4+af+A15J3y2+PEHIvEoLWXxbGKEsX4c81rALn1w=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.274", "", { "os": "linux", "cpu": "arm64" }, "sha512-kYsmSN6zieDwVS3QZBBay9fxNp+h4t4OXleuk6xWhrUDEQRfRQEFlD7JVWWWjJIiHILnniycOxarwo4VpqTf1Q=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.240", "", { "os": "linux", "cpu": "arm64" }, "sha512-EsshHPGUtkuw4SAIFnj0zUDfb3Kvp/wPPtVyF0i+rjvekY99ZiQmLZeAmNWf1qTdktYlNiQZE2D77qUW+kaOYA=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.274", "", { "os": "linux", "cpu": "arm64" }, "sha512-pNjZQRF3f1O5JwnoifuAD7tSRfk9LSwwBzEPiUHB4sJgatF6hzqyDgTN05hcDx28xg44j0jpKpyVwsjveI/s4g=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.240", "", { "os": "linux", "cpu": "x64" }, "sha512-pp3VLi/AuZhYn4SY0cMXRHdJam3B7US423OhrV90qlJzNdJ389urxgfQSbuc38wh4nRpbo3xD1uz83qFD/MSag=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.274", "", { "os": "linux", "cpu": "x64" }, "sha512-QLeK9LzTtTdCjCDRYUVRHj1I3U6AfMvpXYI4Ur1s+vrUcad/C20kShS5/kgug+rERU5CNxNA9v0A052rt47ZXQ=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.240", "", { "os": "linux", "cpu": "x64" }, "sha512-wcuLoslY5vepqOALUKhQ/7mTaMqqPD/iCqZ74BwSRjgaylMoNKIkrdKMIcJwewgrcPuiAdJOYEdYGUaWYX9a2Q=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.274", "", { "os": "linux", "cpu": "x64" }, "sha512-a9eIkmgJ5sSywHwWJcComogjIMsfHsurckQ85ljPNTUSFguzl/ZvbTXGiWNRK5rHsoNd3amwQf2fVC/vFyv3eg=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.240", "", { "os": "win32", "cpu": "arm64" }, "sha512-HbXGvQFCB7DTdlTq/JRlpgQBtTjTnMirg28LdLvvXaZL492cZPAvk8CjhzgvRlwQaHv4qpUvkR33R2xlyiBGkg=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.274", "", { "os": "win32", "cpu": "arm64" }, "sha512-A5wvZAXL3nhuabIXUcRuoYhuPPdEIt3LoA3jUkVLT3c0H+7SH0rv5bylpM9n+apf7lwLCm2TdMZ6x1Ze1Vzekg=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.240", "", { "os": "win32", "cpu": "x64" }, "sha512-XSM+BTBmYi6a06h/ERReyn+oLRrF2JvkbB/BLC1H0GcRwdsiv4r3bzwy0rLcR2OXNzPL8aZ5C5hOG6mqUFiazg=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.274", "", { "os": "win32", "cpu": "x64" }, "sha512-dGcaLfEpV4kwcSijPAIJru+iEiVhHvbBdkyixPyhhf8pniV9B62nUMFFsjNtYL37xIPHxixh2Gsx9/A+Yk4oww=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.93.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-q9vaSZQVFx6B/gPxetGYfLXSJD5v0sOmh0OpZDq7yCrTSA+Rscvrtyol7JJTW40wEpQB4U1B4JXzxQitbQ3CAA=="],
+1 -1
View File
@@ -11,7 +11,7 @@
},
"dependencies": {
"@actions/core": "^1.10.1",
"@anthropic-ai/claude-agent-sdk": "^0.3.240",
"@anthropic-ai/claude-agent-sdk": "^0.3.274",
"shell-quote": "^1.8.4"
},
"devDependencies": {
+10 -10
View File
@@ -7,7 +7,7 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "^0.3.240",
"@anthropic-ai/claude-agent-sdk": "^0.3.274",
"@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1",
@@ -37,23 +37,23 @@
"@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.3.240", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.240", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.240", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.240", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.240", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.240" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-rt/n4rr/Iqem0JtDdL7DdzDnMEei652sS00wVwZ3/0btFt9ah5/iKPKZi8udJ2fjiBwJk6spz9vUQrm6LeyXsA=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.274", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.274", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.274", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.274", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.274", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.274" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-kFmWMsh/BEd4jKkOxUeihr0xMkaMdOxONhNNkZ2pGmP0ElHkArvFvuZxqJTPvREnhCsPZByv5f0EzyAb/hkRZw=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.240", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iCGku/IOMvvwSfdFQXnkX2Txzo6nNzz/JoJ2tvmPSnA2NvEEKbSL2P7/WCGSo8asKe1m/6S6EDhYbiagyhc5VQ=="],
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.274", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B0uAdUIbUhWuybOT5FkhbdsALGNduyaHYo+bEggKBSQS3RFRjgNbX/ah1+IwkmTn4UKDN7AX86DkTEjpj6TSWQ=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.240", "", { "os": "darwin", "cpu": "x64" }, "sha512-rA5uZtNGbpxe2+ghzBCZvB30Q4tcFduxkP1qMjI4jOFNq5M4b0O173MaQj+g8D9o0clKbAIEx3tq8pSrtEXotA=="],
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.274", "", { "os": "darwin", "cpu": "x64" }, "sha512-l5pp3Z+z2mti0H3y1O0MRbTg/r+MDm0hly4WpoKmZyIZSOrDQR7jhCqS6Ihj3Vvd7CMP9NZ8t6F0oXVBDCM6YQ=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.240", "", { "os": "linux", "cpu": "arm64" }, "sha512-cvfxos8HdvVlHO9vKpt8i5cC87a5EuLFBtzjtr+ycczipp4+af+A15J3y2+PEHIvEoLWXxbGKEsX4c81rALn1w=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.274", "", { "os": "linux", "cpu": "arm64" }, "sha512-kYsmSN6zieDwVS3QZBBay9fxNp+h4t4OXleuk6xWhrUDEQRfRQEFlD7JVWWWjJIiHILnniycOxarwo4VpqTf1Q=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.240", "", { "os": "linux", "cpu": "arm64" }, "sha512-EsshHPGUtkuw4SAIFnj0zUDfb3Kvp/wPPtVyF0i+rjvekY99ZiQmLZeAmNWf1qTdktYlNiQZE2D77qUW+kaOYA=="],
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.274", "", { "os": "linux", "cpu": "arm64" }, "sha512-pNjZQRF3f1O5JwnoifuAD7tSRfk9LSwwBzEPiUHB4sJgatF6hzqyDgTN05hcDx28xg44j0jpKpyVwsjveI/s4g=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.240", "", { "os": "linux", "cpu": "x64" }, "sha512-pp3VLi/AuZhYn4SY0cMXRHdJam3B7US423OhrV90qlJzNdJ389urxgfQSbuc38wh4nRpbo3xD1uz83qFD/MSag=="],
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.274", "", { "os": "linux", "cpu": "x64" }, "sha512-QLeK9LzTtTdCjCDRYUVRHj1I3U6AfMvpXYI4Ur1s+vrUcad/C20kShS5/kgug+rERU5CNxNA9v0A052rt47ZXQ=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.240", "", { "os": "linux", "cpu": "x64" }, "sha512-wcuLoslY5vepqOALUKhQ/7mTaMqqPD/iCqZ74BwSRjgaylMoNKIkrdKMIcJwewgrcPuiAdJOYEdYGUaWYX9a2Q=="],
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.274", "", { "os": "linux", "cpu": "x64" }, "sha512-a9eIkmgJ5sSywHwWJcComogjIMsfHsurckQ85ljPNTUSFguzl/ZvbTXGiWNRK5rHsoNd3amwQf2fVC/vFyv3eg=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.240", "", { "os": "win32", "cpu": "arm64" }, "sha512-HbXGvQFCB7DTdlTq/JRlpgQBtTjTnMirg28LdLvvXaZL492cZPAvk8CjhzgvRlwQaHv4qpUvkR33R2xlyiBGkg=="],
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.274", "", { "os": "win32", "cpu": "arm64" }, "sha512-A5wvZAXL3nhuabIXUcRuoYhuPPdEIt3LoA3jUkVLT3c0H+7SH0rv5bylpM9n+apf7lwLCm2TdMZ6x1Ze1Vzekg=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.240", "", { "os": "win32", "cpu": "x64" }, "sha512-XSM+BTBmYi6a06h/ERReyn+oLRrF2JvkbB/BLC1H0GcRwdsiv4r3bzwy0rLcR2OXNzPL8aZ5C5hOG6mqUFiazg=="],
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.274", "", { "os": "win32", "cpu": "x64" }, "sha512-dGcaLfEpV4kwcSijPAIJru+iEiVhHvbBdkyixPyhhf8pniV9B62nUMFFsjNtYL37xIPHxixh2Gsx9/A+Yk4oww=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.93.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-q9vaSZQVFx6B/gPxetGYfLXSJD5v0sOmh0OpZDq7yCrTSA+Rscvrtyol7JJTW40wEpQB4U1B4JXzxQitbQ3CAA=="],
+1 -1
View File
@@ -12,7 +12,7 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "^0.3.240",
"@anthropic-ai/claude-agent-sdk": "^0.3.274",
"@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1",
+1 -1
View File
@@ -824,7 +824,7 @@ ${
? `- Use mcp__github_file_ops__commit_files for making commits (works for both new and existing files, single or multiple). Use mcp__github_file_ops__delete_files for deleting files (supports deleting single or multiple files atomically), or mcp__github__delete_file for deleting a single file. Edit files locally, and the tool will read the content from the same path on disk.
Tool usage examples:
- mcp__github_file_ops__commit_files: {"files": ["path/to/file1.js", "path/to/file2.py"], "message": "feat: add new feature"}
- mcp__github_file_ops__delete_files: {"files": ["path/to/old.js"], "message": "chore: remove deprecated file"}`
- mcp__github_file_ops__delete_files: {"paths": ["path/to/old.js"], "message": "chore: remove deprecated file"}`
: `- Use git commands via the Bash tool for version control (remember that you have access to these git commands):
- Stage files: Bash(git add <files>)
- Commit changes: Bash(git commit -m "<message>")
+1 -1
View File
@@ -77,7 +77,7 @@ async function installClaudeCode(): Promise<string> {
return customExecutable;
}
const claudeCodeVersion = "2.1.240";
const claudeCodeVersion = "2.1.274";
console.log(`Installing Claude Code v${claudeCodeVersion}...`);
for (let attempt = 1; attempt <= 3; attempt++) {
+2 -1
View File
@@ -16,6 +16,7 @@ import type { ParsedGitHubContext } from "../github/context";
import { GITHUB_SERVER_URL } from "../github/api/config";
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
import { encodeBranchNameForUrl } from "../github/operations/comments/common";
export type UpdateCommentLinkParams = {
commentId: number;
@@ -159,7 +160,7 @@ export async function updateCommentLink(
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${encodeBranchNameForUrl(baseBranch)}...${encodeBranchNameForUrl(claudeBranch)}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
}
} catch (error) {
+5 -4
View File
@@ -1,5 +1,6 @@
import type { Octokits } from "../api/client";
import { GITHUB_SERVER_URL } from "../api/config";
import { encodeBranchNameForUrl } from "./comments/common";
import { $ } from "bun";
export async function checkAndCommitOrDeleteBranch(
@@ -106,7 +107,7 @@ export async function checkAndCommitOrDeleteBranch(
);
// Set branch link since we now have commits
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchNameForUrl(claudeBranch)}`;
branchLink = `\n[View branch](${branchUrl})`;
} else {
console.log(
@@ -117,7 +118,7 @@ export async function checkAndCommitOrDeleteBranch(
} catch (gitError) {
console.error("Error checking/committing changes:", gitError);
// If we can't check git status, assume the branch might have changes
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchNameForUrl(claudeBranch)}`;
branchLink = `\n[View branch](${branchUrl})`;
}
} else {
@@ -128,13 +129,13 @@ export async function checkAndCommitOrDeleteBranch(
}
} else {
// Only add branch link if there are commits
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchNameForUrl(claudeBranch)}`;
branchLink = `\n[View branch](${branchUrl})`;
}
} catch (error) {
console.error("Error comparing commits on Claude branch:", error);
// If we can't compare but the branch exists remotely, include the branch link
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchNameForUrl(claudeBranch)}`;
branchLink = `\n[View branch](${branchUrl})`;
}
}
+6 -4
View File
@@ -29,7 +29,7 @@ function extractFirstLabel(githubData: FetchDataResult): string | undefined {
*
* Valid branch names:
* - Start with alphanumeric character, underscore, or @ (not dash, to prevent option injection)
* - Contain only alphanumeric, forward slash, hyphen, underscore, period, hash (#), plus (+), comma (,), or at sign (@)
* - Contain only alphanumeric, forward slash, hyphen, underscore, period, hash (#), plus (+), comma (,), at sign (@), or parentheses
* - Do not start or end with a period
* - Do not end with a slash
* - Do not contain '..' (path traversal)
@@ -60,7 +60,7 @@ export function validateBranchName(branchName: string): void {
);
}
// Strict whitelist pattern: alphanumeric or @ start, then alphanumeric/slash/hyphen/underscore/period/hash/plus/comma/at-sign.
// Strict whitelist pattern: alphanumeric or @ start, then alphanumeric/slash/hyphen/underscore/period/hash/plus/comma/at-sign/parentheses.
// # is valid per git-check-ref-format and commonly used in branch names like "fix/#123-description".
// + is valid per git-check-ref-format and generated by Claude Code's EnterWorktree tool when
// converting worktree names containing "/" (e.g. "feat/foo" becomes "worktree-feat+foo").
@@ -72,12 +72,14 @@ export function validateBranchName(branchName: string): void {
// _ is valid per git-check-ref-format anywhere in a ref name, including the first character;
// leading underscores are a common convention for release/internal branches (e.g.
// "_release/v1.2.3"), which previously failed validation as a PR's base branch.
// Parentheses are valid per git-check-ref-format and commonly appear in branch names that
// use Conventional Commit-style scopes (e.g. "feat(parser)-handle-empty-input").
// All git calls use execFileSync (not shell interpolation), so none of these characters carry injection risk.
const validPattern = /^[a-zA-Z0-9@_][a-zA-Z0-9/_.#+,@-]*$/;
const validPattern = /^[a-zA-Z0-9@_][a-zA-Z0-9/_.#+,@()-]*$/;
if (!validPattern.test(branchName)) {
throw new Error(
`Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character, underscore, or '@' and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), plus signs (+), commas (,), or at signs (@).`,
`Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character, underscore, or '@' and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), plus signs (+), commas (,), at signs (@), or parentheses.`,
);
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { GITHUB_SERVER_URL } from "../api/config";
import { redactSecrets } from "../utils/sanitizer";
import { encodeBranchNameForUrl } from "./comments/common";
export type ExecutionDetails = {
total_cost_usd?: number;
@@ -161,7 +162,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
// Extract owner/repo from jobUrl
const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//);
if (repoMatch) {
branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`;
branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${encodeBranchNameForUrl(finalBranchName)}`;
}
}
+6 -1
View File
@@ -12,12 +12,17 @@ export function createJobRunLink(
return `[View job run](${jobRunUrl})`;
}
/** Encode Git-ref path segments without turning `/` into `%2F`. */
export function encodeBranchNameForUrl(branchName: string): string {
return branchName.split("/").map(encodeURIComponent).join("/");
}
export function createBranchLink(
owner: string,
repo: string,
branchName: string,
): string {
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${branchName}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchNameForUrl(branchName)}`;
return `\n[View branch](${branchUrl})`;
}
+56 -26
View File
@@ -17,11 +17,18 @@ const PR_NUMBER = process.env.PR_NUMBER;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const RUNNER_TEMP = process.env.RUNNER_TEMP || "/tmp";
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER || !GITHUB_TOKEN) {
console.error(
"[GitHub CI Server] Error: REPO_OWNER, REPO_NAME, PR_NUMBER, and GITHUB_TOKEN environment variables are required",
);
process.exit(1);
// Job logs are fetched by ID from GitHub-hosted storage; bound the request so a
// stalled fetch can't hang this MCP call forever. Mirrors the timeout added to
// fetchImage() in src/github/utils/image-downloader.ts (#1625).
const DOWNLOAD_JOB_LOG_TIMEOUT_MS = 30_000;
if (import.meta.main) {
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER || !GITHUB_TOKEN) {
console.error(
"[GitHub CI Server] Error: REPO_OWNER, REPO_NAME, PR_NUMBER, and GITHUB_TOKEN environment variables are required",
);
process.exit(1);
}
}
const server = new McpServer({
@@ -205,6 +212,40 @@ server.tool(
},
);
export async function downloadJobLog(
client: Octokit,
params: { owner: string; repo: string; job_id: number },
runnerTemp: string,
timeoutMs: number = DOWNLOAD_JOB_LOG_TIMEOUT_MS,
): Promise<{ path: string; size_bytes: number }> {
const controller = new AbortController();
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await client.actions.downloadJobLogsForWorkflowRun({
owner: params.owner,
repo: params.repo,
job_id: params.job_id,
request: { signal: controller.signal },
});
const logsText = response.data as unknown as string;
const logsDir = `${runnerTemp}/github-ci-logs`;
await mkdir(logsDir, { recursive: true });
const logPath = `${logsDir}/job-${params.job_id}.log`;
await writeFile(logPath, logsText, "utf-8");
return {
path: logPath,
size_bytes: Buffer.byteLength(logsText, "utf-8"),
};
} finally {
clearTimeout(timeoutHandle);
}
}
server.tool(
"download_job_log",
"Download job logs to disk",
@@ -218,24 +259,11 @@ server.tool(
baseUrl: GITHUB_API_URL,
});
const response = await client.actions.downloadJobLogsForWorkflowRun({
owner: REPO_OWNER!,
repo: REPO_NAME!,
job_id,
});
const logsText = response.data as unknown as string;
const logsDir = `${RUNNER_TEMP}/github-ci-logs`;
await mkdir(logsDir, { recursive: true });
const logPath = `${logsDir}/job-${job_id}.log`;
await writeFile(logPath, logsText, "utf-8");
const result = {
path: logPath,
size_bytes: Buffer.byteLength(logsText, "utf-8"),
};
const result = await downloadJobLog(
client,
{ owner: REPO_OWNER!, repo: REPO_NAME!, job_id },
RUNNER_TEMP,
);
return {
content: [
@@ -277,6 +305,8 @@ async function runServer() {
}
}
runServer().catch(() => {
process.exit(1);
});
if (import.meta.main) {
runServer().catch(() => {
process.exit(1);
});
}
+24
View File
@@ -0,0 +1,24 @@
import { z } from "zod";
/** Raw shape passed to `server.tool` for commit_files. */
export const commitFilesInputSchema = {
files: z
.array(z.string())
.describe(
'Array of file paths relative to repository root (e.g. ["src/main.js", "README.md"]). All files must exist locally.',
),
message: z.string().describe("Commit message"),
};
/** Raw shape passed to `server.tool` for delete_files. */
export const deleteFilesInputSchema = {
paths: z
.array(z.string())
.describe(
'Array of file paths to delete relative to repository root (e.g. ["src/old-file.js", "docs/deprecated.md"])',
),
message: z.string().describe("Commit message"),
};
export const commitFilesPayloadSchema = z.object(commitFilesInputSchema);
export const deleteFilesPayloadSchema = z.object(deleteFilesInputSchema);
+6 -17
View File
@@ -2,7 +2,6 @@
// GitHub File Operations MCP Server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFile, stat } from "fs/promises";
import { resolve } from "path";
import { constants } from "fs";
@@ -11,6 +10,10 @@ import { GITHUB_API_URL } from "../github/api/config";
import { isBinaryContent } from "./binary-detection";
import { validatePathWithinRepo } from "./path-validation";
import { updateGitReference } from "./update-git-reference";
import {
commitFilesInputSchema,
deleteFilesInputSchema,
} from "./github-file-ops-schemas";
type GitHubRef = {
object: {
@@ -197,14 +200,7 @@ async function getFileMode(filePath: string): Promise<string> {
server.tool(
"commit_files",
"Commit one or more files to a repository in a single commit (this will commit them atomically in the remote repository)",
{
files: z
.array(z.string())
.describe(
'Array of file paths relative to repository root (e.g. ["src/main.js", "README.md"]). All files must exist locally.',
),
message: z.string().describe("Commit message"),
},
commitFilesInputSchema,
async ({ files, message }) => {
const owner = REPO_OWNER;
const repo = REPO_NAME;
@@ -414,14 +410,7 @@ server.tool(
server.tool(
"delete_files",
"Delete one or more files from a repository in a single commit",
{
paths: z
.array(z.string())
.describe(
'Array of file paths to delete relative to repository root (e.g. ["src/old-file.js", "docs/deprecated.md"])',
),
message: z.string().describe("Commit message"),
},
deleteFilesInputSchema,
async ({ paths, message }) => {
const owner = REPO_OWNER;
const repo = REPO_NAME;
+6
View File
@@ -36,6 +36,12 @@ describe("comments/common", () => {
);
});
test("encodes URL-significant characters in a branch name", () => {
expect(createBranchLink("o", "r", "claude/fix#123")).toBe(
`\n[View branch](${GITHUB_SERVER_URL}/o/r/tree/claude/fix%23123)`,
);
});
test("prefixes the link with a newline so it renders on its own line", () => {
expect(createBranchLink("o", "r", "main").startsWith("\n")).toBe(true);
});
+6
View File
@@ -819,6 +819,12 @@ describe("generatePrompt", () => {
// Should have commit signing tool instructions
expect(prompt).toContain("mcp__github_file_ops__commit_files");
expect(prompt).toContain("mcp__github_file_ops__delete_files");
expect(prompt).toContain(
'mcp__github_file_ops__delete_files: {"paths": ["path/to/old.js"]',
);
expect(prompt).not.toContain(
'mcp__github_file_ops__delete_files: {"files":',
);
// Comment tool should always be from comment server, not file ops
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bun
import { describe, expect, test, beforeAll } from "bun:test";
import { generatePrompt } from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
import {
commitFilesPayloadSchema,
deleteFilesPayloadSchema,
} from "../src/mcp/github-file-ops-schemas";
beforeAll(() => {
process.env.GITHUB_ACTION_PATH = "/test/action/path";
});
const mockGitHubData = {
contextData: {
title: "Test PR",
body: "This is a test PR",
author: { login: "testuser" },
state: "OPEN",
labels: { nodes: [] },
createdAt: "2023-01-01T00:00:00Z",
additions: 15,
deletions: 5,
baseRefName: "main",
headRefName: "feature-branch",
headRefOid: "abc123",
isCrossRepository: false,
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
commits: { totalCount: 0, nodes: [] },
files: { nodes: [] },
comments: { nodes: [] },
reviews: { nodes: [] },
},
comments: [],
changedFiles: [],
changedFilesWithSHA: [],
reviewData: null,
imageUrlMap: new Map<string, string>(),
};
const signingContext: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issue_comment",
commentId: "67890",
isPR: true,
prNumber: "123",
commentBody: "@claude delete the old file",
},
};
function extractToolExample(
prompt: string,
tool: string,
): Record<string, unknown> {
const match = prompt.match(
new RegExp(
`${tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*(\\{[^}]+\\})`,
),
);
if (!match) {
throw new Error(`No JSON example for ${tool} in prompt`);
}
return JSON.parse(match[1] as string);
}
describe("delete_files prompt vs live MCP schema (#1665)", () => {
test("the payload the old prompt taught is rejected by the tool schema", () => {
const taughtByOldPrompt = {
files: ["path/to/old.js"],
message: "chore: remove deprecated file",
};
const result = deleteFilesPayloadSchema.safeParse(taughtByOldPrompt);
expect(result.success).toBe(false);
if (!result.success) {
const fields = result.error.issues.map((issue) => issue.path.join("."));
expect(fields).toContain("paths");
}
});
test("the payload the new prompt teaches is accepted by the tool schema", () => {
const taughtByNewPrompt = {
paths: ["path/to/old.js"],
message: "chore: remove deprecated file",
};
const result = deleteFilesPayloadSchema.safeParse(taughtByNewPrompt);
expect(result.success).toBe(true);
});
test("generated tag-mode prompt example parses against the live schema", async () => {
const prompt = await generatePrompt(
signingContext,
mockGitHubData,
true,
"tag",
);
const example = extractToolExample(
prompt,
"mcp__github_file_ops__delete_files",
);
expect(example).toHaveProperty("paths");
expect(example).not.toHaveProperty("files");
const result = deleteFilesPayloadSchema.safeParse(example);
expect(result.success).toBe(true);
});
test("rejects paths when the value is a string instead of an array", () => {
const result = deleteFilesPayloadSchema.safeParse({
paths: "path/to/old.js",
message: "chore: remove deprecated file",
});
expect(result.success).toBe(false);
});
test("rejects a payload that has paths but omits message", () => {
const result = deleteFilesPayloadSchema.safeParse({
paths: ["path/to/old.js"],
});
expect(result.success).toBe(false);
if (!result.success) {
expect(
result.error.issues.map((issue) => issue.path.join(".")),
).toContain("message");
}
});
test("accepts a payload that still includes the old files key beside paths", () => {
const result = deleteFilesPayloadSchema.safeParse({
files: ["path/to/old.js"],
paths: ["path/to/old.js"],
message: "chore: remove deprecated file",
});
expect(result.success).toBe(true);
});
test("does not change commit_files — that sibling tool still requires files", async () => {
const prompt = await generatePrompt(
signingContext,
mockGitHubData,
true,
"tag",
);
const example = extractToolExample(
prompt,
"mcp__github_file_ops__commit_files",
);
expect(example).toHaveProperty("files");
expect(example).not.toHaveProperty("paths");
expect(commitFilesPayloadSchema.safeParse(example).success).toBe(true);
expect(deleteFilesPayloadSchema.safeParse(example).success).toBe(false);
});
test("generated delete_files example keys are exactly paths and message", async () => {
const prompt = await generatePrompt(
signingContext,
mockGitHubData,
true,
"tag",
);
const example = extractToolExample(
prompt,
"mcp__github_file_ops__delete_files",
);
expect(Object.keys(example).sort()).toEqual(["message", "paths"]);
});
});
+94
View File
@@ -0,0 +1,94 @@
import { describe, test, expect, afterEach } from "bun:test";
import { readFile, rm } from "fs/promises";
import os from "os";
import path from "path";
import { downloadJobLog } from "../src/mcp/github-actions-server";
import type { Octokit } from "@octokit/rest";
describe("downloadJobLog", () => {
const tmpDirs: string[] = [];
const makeRunnerTemp = () => {
const dir = path.join(
os.tmpdir(),
`download-job-log-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
tmpDirs.push(dir);
return dir;
};
afterEach(async () => {
while (tmpDirs.length) {
const dir = tmpDirs.pop()!;
await rm(dir, { recursive: true, force: true });
}
});
const createStallingClient = (): {
client: Octokit;
getSignal: () => AbortSignal | undefined;
} => {
let signal: AbortSignal | undefined;
const client = {
actions: {
downloadJobLogsForWorkflowRun: (params: {
request?: { signal?: AbortSignal };
}) => {
signal = params.request?.signal;
return new Promise((_resolve, reject) => {
signal?.addEventListener("abort", () => {
reject(new Error("This operation was aborted"));
});
// Otherwise never settles, simulating a stalled fetch.
});
},
},
} as unknown as Octokit;
return { client, getSignal: () => signal };
};
test("rejects with a timeout instead of hanging when the download stalls", async () => {
const { client, getSignal } = createStallingClient();
const runnerTemp = makeRunnerTemp();
await expect(
downloadJobLog(
client,
{ owner: "owner", repo: "repo", job_id: 123 },
runnerTemp,
5,
),
).rejects.toThrow();
expect(getSignal()?.aborted).toBe(true);
});
test("writes the log to disk and clears the timeout when the download succeeds", async () => {
const runnerTemp = makeRunnerTemp();
const client = {
actions: {
downloadJobLogsForWorkflowRun: async (params: {
request?: { signal?: AbortSignal };
}) => {
expect(params.request?.signal?.aborted).toBe(false);
return { data: "log line 1\nlog line 2\n" };
},
},
} as unknown as Octokit;
const result = await downloadJobLog(
client,
{ owner: "owner", repo: "repo", job_id: 456 },
runnerTemp,
30_000,
);
expect(result.path).toBe(`${runnerTemp}/github-ci-logs/job-456.log`);
expect(result.size_bytes).toBe(
Buffer.byteLength("log line 1\nlog line 2\n", "utf-8"),
);
const written = await readFile(result.path, "utf-8");
expect(written).toBe("log line 1\nlog line 2\n");
});
});
+9
View File
@@ -29,6 +29,15 @@ describe("validateBranchName", () => {
expect(() => validateBranchName("release.1.2.3")).not.toThrow();
});
it("should accept branch names containing parentheses", () => {
expect(() =>
validateBranchName("feat(example)-valid-branch"),
).not.toThrow();
expect(() =>
validateBranchName("fix(parser)-handle-empty-input"),
).not.toThrow();
});
it("should accept typical branch name formats", () => {
expect(() =>
validateBranchName("claude/issue-123-20250101-1234"),