mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-09-17 23:10:47 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b8197d3d4 | ||
|
|
7b0b255830 | ||
|
|
bf38e86e58 | ||
|
|
51db78a4b8 | ||
|
|
9cdae7f0d9 | ||
|
|
56cf60fde4 | ||
|
|
0a8d3c9443 | ||
|
|
19dda84776 | ||
|
|
5ccc3a35a6 | ||
|
|
0d0e0876d3 | ||
|
|
9c5ddab2e6 | ||
|
|
d75b94d5ad | ||
|
|
ef8bb1e43b | ||
|
|
fa2b2666b7 | ||
|
|
8251c103ac | ||
|
|
781d62e9d5 | ||
|
|
833fb0f8c9 | ||
|
|
a874e9ecd7 | ||
|
|
a60f3e1db3 | ||
|
|
e8c2d7c16c | ||
|
|
70fec18385 | ||
|
|
1f291e1cfe | ||
|
|
76ac41a83e | ||
|
|
8ef9699156 | ||
|
|
791545dab1 | ||
|
|
2d7a787fbd | ||
|
|
b58c16b325 | ||
|
|
16b3b310c3 | ||
|
|
6bcfb8263a | ||
|
|
b62c7454dc | ||
|
|
e5ad3c7725 | ||
|
|
c81e3bc69d |
@@ -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
|
||||
@@ -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
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>")
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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})`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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})`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user