Merge branch 'main' into dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.52.5
This commit is contained in:
@@ -28,11 +28,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
id: setup-node
|
id: setup-node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
- if: ${{ failure() && steps.diff.outcome == 'failure' }}
|
- if: ${{ failure() && steps.diff.outcome == 'failure' }}
|
||||||
name: Upload Artifact
|
name: Upload Artifact
|
||||||
id: upload
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
id: setup-node
|
id: setup-node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -54,22 +54,53 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .node-version
|
||||||
|
|
||||||
|
- name: Start Mock Inference Server
|
||||||
|
id: mock-server
|
||||||
|
run: |
|
||||||
|
node script/mock-inference-server.mjs &
|
||||||
|
echo "pid=$!" >> $GITHUB_OUTPUT
|
||||||
|
# Wait for server to be ready
|
||||||
|
for i in {1..10}; do
|
||||||
|
if curl -s http://localhost:3456/health > /dev/null; then
|
||||||
|
echo "Mock server is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
- name: Test Local Action
|
- name: Test Local Action
|
||||||
id: test-action
|
id: test-action
|
||||||
continue-on-error: true
|
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
prompt: hello
|
prompt: hello
|
||||||
|
endpoint: http://localhost:3456
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
- name: Print Output
|
- name: Print Output
|
||||||
id: output
|
id: output
|
||||||
continue-on-error: true
|
|
||||||
run: echo "${{ steps.test-action.outputs.response }}"
|
run: echo "${{ steps.test-action.outputs.response }}"
|
||||||
|
|
||||||
|
- name: Verify Output
|
||||||
|
run: |
|
||||||
|
response="${{ steps.test-action.outputs.response }}"
|
||||||
|
if [[ -z "$response" ]]; then
|
||||||
|
echo "Error: No response received"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Response received: $response"
|
||||||
|
|
||||||
|
- name: Stop Mock Server
|
||||||
|
if: always()
|
||||||
|
run: kill ${{ steps.mock-server.outputs.pid }} || true
|
||||||
|
|
||||||
test-action-prompt-file:
|
test-action-prompt-file:
|
||||||
name: GitHub Actions Test with Prompt File
|
name: GitHub Actions Test with Prompt File
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -77,7 +108,26 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .node-version
|
||||||
|
|
||||||
|
- name: Start Mock Inference Server
|
||||||
|
id: mock-server
|
||||||
|
run: |
|
||||||
|
node script/mock-inference-server.mjs &
|
||||||
|
echo "pid=$!" >> $GITHUB_OUTPUT
|
||||||
|
# Wait for server to be ready
|
||||||
|
for i in {1..10}; do
|
||||||
|
if curl -s http://localhost:3456/health > /dev/null; then
|
||||||
|
echo "Mock server is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
- name: Create Prompt File
|
- name: Create Prompt File
|
||||||
run: echo "hello" > prompt.txt
|
run: echo "hello" > prompt.txt
|
||||||
@@ -87,16 +137,33 @@ jobs:
|
|||||||
|
|
||||||
- name: Test Local Action with Prompt File
|
- name: Test Local Action with Prompt File
|
||||||
id: test-action-prompt-file
|
id: test-action-prompt-file
|
||||||
continue-on-error: true
|
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
prompt-file: prompt.txt
|
prompt-file: prompt.txt
|
||||||
system-prompt-file: system-prompt.txt
|
system-prompt-file: system-prompt.txt
|
||||||
|
endpoint: http://localhost:3456
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
- name: Print Output
|
- name: Print Output
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
echo "Response saved to: ${{ steps.test-action-prompt-file.outputs.response-file }}"
|
echo "Response saved to: ${{ steps.test-action-prompt-file.outputs.response-file }}"
|
||||||
cat "${{ steps.test-action-prompt-file.outputs.response-file }}"
|
cat "${{ steps.test-action-prompt-file.outputs.response-file }}"
|
||||||
|
|
||||||
|
- name: Verify Output
|
||||||
|
run: |
|
||||||
|
response_file="${{ steps.test-action-prompt-file.outputs.response-file }}"
|
||||||
|
if [[ ! -f "$response_file" ]]; then
|
||||||
|
echo "Error: Response file not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
content=$(cat "$response_file")
|
||||||
|
if [[ -z "$content" ]]; then
|
||||||
|
echo "Error: Response file is empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Response file content: $content"
|
||||||
|
|
||||||
|
- name: Stop Mock Server
|
||||||
|
if: always()
|
||||||
|
run: kill ${{ steps.mock-server.outputs.pid }} || true
|
||||||
|
|||||||
@@ -30,19 +30,19 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
id: initialize
|
id: initialize
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
source-root: src
|
source-root: src
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
id: autobuild
|
id: autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
id: analyze
|
id: analyze
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
id: setup-node
|
id: setup-node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version-file: .node-version
|
node-version-file: .node-version
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
id: setup-ruby
|
id: setup-ruby
|
||||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4
|
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71
|
||||||
with:
|
with:
|
||||||
ruby-version: ruby
|
ruby-version: ruby
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: qs
|
name: qs
|
||||||
version: 6.14.0
|
version: 6.14.1
|
||||||
type: npm
|
type: npm
|
||||||
summary: A querystring parser that supports nesting and arrays, with a depth limit
|
summary: A querystring parser that supports nesting and arrays, with a depth limit
|
||||||
homepage: https://github.com/ljharb/qs
|
homepage: https://github.com/ljharb/qs
|
||||||
|
|||||||
@@ -162,8 +162,18 @@ This action now supports **read-only** integration with the GitHub-hosted Model
|
|||||||
Context Protocol (MCP) server, which provides access to GitHub tools like
|
Context Protocol (MCP) server, which provides access to GitHub tools like
|
||||||
repository management, issue tracking, and pull request operations.
|
repository management, issue tracking, and pull request operations.
|
||||||
|
|
||||||
> [!NOTE]
|
#### Authentication
|
||||||
> The GitHub MCP integration requires a Personal Access Token (PAT) and cannot use the built-in `GITHUB_TOKEN`.
|
|
||||||
|
You can authenticate the MCP server with **either**:
|
||||||
|
|
||||||
|
1. **Personal Access Token (PAT)** – user-scoped token
|
||||||
|
2. **GitHub App Installation Token** (`ghs_…`) – short-lived, app-scoped token
|
||||||
|
> The built-in `GITHUB_TOKEN` is **not** accepted by the MCP server.
|
||||||
|
> Using a **GitHub App installation token** is recommended in most CI environments because it is short-lived and least-privilege by design.
|
||||||
|
|
||||||
|
#### Enabling MCP in the action
|
||||||
|
|
||||||
|
Set `enable-github-mcp: true` and provide a token via `github-mcp-token`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
@@ -173,7 +183,7 @@ steps:
|
|||||||
with:
|
with:
|
||||||
prompt: 'List my open pull requests and create a summary'
|
prompt: 'List my open pull requests and create a summary'
|
||||||
enable-github-mcp: true
|
enable-github-mcp: true
|
||||||
token: ${{ secrets.USER_PAT }}
|
token: ${{ secrets.USER_PAT }} # or a ghs_ installation token
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want, you can use separate tokens for the AI inference endpoint
|
If you want, you can use separate tokens for the AI inference endpoint
|
||||||
@@ -188,9 +198,28 @@ steps:
|
|||||||
prompt: 'List my open pull requests and create a summary'
|
prompt: 'List my open pull requests and create a summary'
|
||||||
enable-github-mcp: true
|
enable-github-mcp: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
github-mcp-token: ${{ secrets.USER_PAT }}
|
github-mcp-token: ${{ secrets.USER_PAT }} # or a ghs_ installation token
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Configuring GitHub MCP Toolsets
|
||||||
|
|
||||||
|
By default, the GitHub MCP server provides a standard set of tools (`context`, `repos`, `issues`, `pull_requests`, `users`). You can customize which toolsets are available by specifying the `github-mcp-toolsets` parameter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- name: AI Inference with Custom Toolsets
|
||||||
|
id: inference
|
||||||
|
uses: actions/ai-inference@v2
|
||||||
|
with:
|
||||||
|
prompt: 'Analyze recent workflow runs and check security alerts'
|
||||||
|
enable-github-mcp: true
|
||||||
|
token: ${{ secrets.USER_PAT }}
|
||||||
|
github-mcp-toolsets: 'repos,issues,pull_requests,actions,code_security'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available toolsets:**
|
||||||
|
See: [Tool configuration](https://github.com/github/github-mcp-server/blob/main/README.md#tool-configuration)
|
||||||
|
|
||||||
When MCP is enabled, the AI model will have access to GitHub tools and can
|
When MCP is enabled, the AI model will have access to GitHub tools and can
|
||||||
perform actions like searching issues and PRs.
|
perform actions like searching issues and PRs.
|
||||||
|
|
||||||
@@ -212,7 +241,7 @@ the action:
|
|||||||
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
|
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
|
||||||
| `max-tokens` | The max number of tokens to generate | 200 |
|
| `max-tokens` | The max number of tokens to generate | 200 |
|
||||||
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
|
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
|
||||||
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT for MCP to work | `""` |
|
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). | `""` |
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
'gpt-4',
|
'gpt-4',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
100,
|
100,
|
||||||
'https://api.test.com',
|
'https://api.test.com',
|
||||||
'test-token',
|
'test-token',
|
||||||
@@ -117,6 +119,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
{role: 'user', content: 'User message'},
|
{role: 'user', content: 'User message'},
|
||||||
],
|
],
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-4',
|
||||||
|
temperature: undefined,
|
||||||
|
topP: undefined,
|
||||||
maxTokens: 100,
|
maxTokens: 100,
|
||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'test-token',
|
token: 'test-token',
|
||||||
@@ -136,6 +140,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
'System prompt',
|
'System prompt',
|
||||||
'User prompt',
|
'User prompt',
|
||||||
'gpt-4',
|
'gpt-4',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
100,
|
100,
|
||||||
'https://api.test.com',
|
'https://api.test.com',
|
||||||
'test-token',
|
'test-token',
|
||||||
@@ -147,6 +153,8 @@ describe('helpers.ts - inference request building', () => {
|
|||||||
{role: 'user', content: 'User prompt'},
|
{role: 'user', content: 'User prompt'},
|
||||||
],
|
],
|
||||||
modelName: 'gpt-4',
|
modelName: 'gpt-4',
|
||||||
|
temperature: undefined,
|
||||||
|
topP: undefined,
|
||||||
maxTokens: 100,
|
maxTokens: 100,
|
||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'test-token',
|
token: 'test-token',
|
||||||
|
|||||||
+6
-28
@@ -75,17 +75,13 @@ vi.mock('fs', () => ({
|
|||||||
writeFileSync: mockWriteFileSync,
|
writeFileSync: mockWriteFileSync,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mocks for tmp module to control temporary file creation and cleanup
|
// Mocks for tmp module to control temporary file creation
|
||||||
const mockRemoveCallback = vi.fn()
|
|
||||||
const mockFileSync = vi.fn().mockReturnValue({
|
const mockFileSync = vi.fn().mockReturnValue({
|
||||||
name: '/secure/temp/dir/modelResponse-abc123.txt',
|
name: '/secure/temp/dir/modelResponse-abc123.txt',
|
||||||
removeCallback: mockRemoveCallback,
|
|
||||||
})
|
})
|
||||||
const mockSetGracefulCleanup = vi.fn()
|
|
||||||
|
|
||||||
vi.mock('tmp', () => ({
|
vi.mock('tmp', () => ({
|
||||||
fileSync: mockFileSync,
|
fileSync: mockFileSync,
|
||||||
setGracefulCleanup: mockSetGracefulCleanup,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock MCP and inference modules
|
// Mock MCP and inference modules
|
||||||
@@ -199,7 +195,7 @@ describe('main.ts', () => {
|
|||||||
|
|
||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
|
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token', '')
|
||||||
expect(mockMcpInference).toHaveBeenCalledWith(
|
expect(mockMcpInference).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
messages: [
|
messages: [
|
||||||
@@ -226,7 +222,7 @@ describe('main.ts', () => {
|
|||||||
|
|
||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
|
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token', '')
|
||||||
expect(mockSimpleInference).toHaveBeenCalled()
|
expect(mockSimpleInference).toHaveBeenCalled()
|
||||||
expect(mockMcpInference).not.toHaveBeenCalled()
|
expect(mockMcpInference).not.toHaveBeenCalled()
|
||||||
expect(core.warning).toHaveBeenCalledWith('MCP connection failed, falling back to simple inference')
|
expect(core.warning).toHaveBeenCalledWith('MCP connection failed, falling back to simple inference')
|
||||||
@@ -283,7 +279,7 @@ describe('main.ts', () => {
|
|||||||
expect(mockProcessExit).toHaveBeenCalledWith(1)
|
expect(mockProcessExit).toHaveBeenCalledWith(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates secure temporary files with proper cleanup', async () => {
|
it('creates temporary files that persist for downstream steps', async () => {
|
||||||
mockInputs({
|
mockInputs({
|
||||||
prompt: 'Test prompt',
|
prompt: 'Test prompt',
|
||||||
'system-prompt': 'You are a test assistant.',
|
'system-prompt': 'You are a test assistant.',
|
||||||
@@ -291,34 +287,16 @@ describe('main.ts', () => {
|
|||||||
|
|
||||||
await run()
|
await run()
|
||||||
|
|
||||||
expect(mockSetGracefulCleanup).toHaveBeenCalledOnce()
|
// Verify temp file is created with keep: true so it persists
|
||||||
|
|
||||||
expect(mockFileSync).toHaveBeenCalledWith({
|
expect(mockFileSync).toHaveBeenCalledWith({
|
||||||
prefix: 'modelResponse-',
|
prefix: 'modelResponse-',
|
||||||
postfix: '.txt',
|
postfix: '.txt',
|
||||||
|
keep: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', '/secure/temp/dir/modelResponse-abc123.txt')
|
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', '/secure/temp/dir/modelResponse-abc123.txt')
|
||||||
expect(mockWriteFileSync).toHaveBeenCalledWith('/secure/temp/dir/modelResponse-abc123.txt', 'Hello, user!', 'utf-8')
|
expect(mockWriteFileSync).toHaveBeenCalledWith('/secure/temp/dir/modelResponse-abc123.txt', 'Hello, user!', 'utf-8')
|
||||||
expect(mockRemoveCallback).toHaveBeenCalledOnce()
|
|
||||||
|
|
||||||
expect(mockProcessExit).toHaveBeenCalledWith(0)
|
expect(mockProcessExit).toHaveBeenCalledWith(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles cleanup errors gracefully', async () => {
|
|
||||||
mockRemoveCallback.mockImplementationOnce(() => {
|
|
||||||
throw new Error('Cleanup failed')
|
|
||||||
})
|
|
||||||
|
|
||||||
mockInputs({
|
|
||||||
prompt: 'Test prompt',
|
|
||||||
'system-prompt': 'You are a test assistant.',
|
|
||||||
})
|
|
||||||
|
|
||||||
await run()
|
|
||||||
|
|
||||||
expect(mockRemoveCallback).toHaveBeenCalledOnce()
|
|
||||||
expect(core.warning).toHaveBeenCalledWith('Failed to cleanup temporary file: Error: Cleanup failed')
|
|
||||||
expect(mockProcessExit).toHaveBeenCalledWith(0)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -113,6 +113,40 @@ describe('mcp.ts', () => {
|
|||||||
expect(result?.tools).toHaveLength(0)
|
expect(result?.tools).toHaveLength(0)
|
||||||
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
|
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses default toolsets when toolsets parameter is not provided', async () => {
|
||||||
|
const token = 'test-token'
|
||||||
|
|
||||||
|
mockConnect.mockResolvedValue(undefined)
|
||||||
|
mockListTools.mockResolvedValue({tools: []})
|
||||||
|
|
||||||
|
await connectToGitHubMCP(token)
|
||||||
|
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Using default GitHub MCP toolsets')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses custom toolsets when toolsets parameter is provided', async () => {
|
||||||
|
const token = 'test-token'
|
||||||
|
const toolsets = 'repos,issues,pull_requests,actions'
|
||||||
|
|
||||||
|
mockConnect.mockResolvedValue(undefined)
|
||||||
|
mockListTools.mockResolvedValue({tools: []})
|
||||||
|
|
||||||
|
await connectToGitHubMCP(token, toolsets)
|
||||||
|
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Using GitHub MCP toolsets: repos,issues,pull_requests,actions')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores empty toolsets parameter', async () => {
|
||||||
|
const token = 'test-token'
|
||||||
|
|
||||||
|
mockConnect.mockResolvedValue(undefined)
|
||||||
|
mockListTools.mockResolvedValue({tools: []})
|
||||||
|
|
||||||
|
await connectToGitHubMCP(token, ' ')
|
||||||
|
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Using default GitHub MCP toolsets')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('executeToolCall', () => {
|
describe('executeToolCall', () => {
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ inputs:
|
|||||||
description: The token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT for MCP to work.
|
description: The token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT for MCP to work.
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
github-mcp-toolsets:
|
||||||
|
description: 'Comma-separated list of toolsets to enable for GitHub MCP (e.g., "repos,issues,pull_requests,actions"). Use "all" for all toolsets, "default" for default set. If not specified, uses default toolsets (context,repos,issues,pull_requests,users).'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
# Define your outputs here.
|
# Define your outputs here.
|
||||||
outputs:
|
outputs:
|
||||||
|
|||||||
+41
-29
@@ -42717,15 +42717,24 @@ class StreamableHTTPClientTransport {
|
|||||||
/**
|
/**
|
||||||
* Connect to the GitHub MCP server and retrieve available tools
|
* Connect to the GitHub MCP server and retrieve available tools
|
||||||
*/
|
*/
|
||||||
async function connectToGitHubMCP(token) {
|
async function connectToGitHubMCP(token, toolsets) {
|
||||||
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/';
|
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/';
|
||||||
coreExports.info('Connecting to GitHub MCP server...');
|
coreExports.info('Connecting to GitHub MCP server...');
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(githubMcpUrl), {
|
const headers = {
|
||||||
requestInit: {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'X-MCP-Readonly': 'true',
|
'X-MCP-Readonly': 'true',
|
||||||
},
|
};
|
||||||
|
// Add toolsets header if specified
|
||||||
|
if (toolsets && toolsets.trim() !== '') {
|
||||||
|
headers['X-MCP-Toolsets'] = toolsets;
|
||||||
|
coreExports.info(`Using GitHub MCP toolsets: ${toolsets}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
coreExports.info('Using default GitHub MCP toolsets');
|
||||||
|
}
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(githubMcpUrl), {
|
||||||
|
requestInit: {
|
||||||
|
headers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
@@ -49496,6 +49505,8 @@ async function simpleInference(request) {
|
|||||||
messages: request.messages,
|
messages: request.messages,
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
};
|
};
|
||||||
// Add response format if specified
|
// Add response format if specified
|
||||||
if (request.responseFormat) {
|
if (request.responseFormat) {
|
||||||
@@ -49530,6 +49541,8 @@ async function mcpInference(request, githubMcpClient) {
|
|||||||
messages: messages,
|
messages: messages,
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
};
|
};
|
||||||
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
||||||
if (finalMessage && request.responseFormat) {
|
if (finalMessage && request.responseFormat) {
|
||||||
@@ -49685,12 +49698,14 @@ function buildResponseFormat(promptConfig) {
|
|||||||
/**
|
/**
|
||||||
* Build complete InferenceRequest from prompt config and inputs
|
* Build complete InferenceRequest from prompt config and inputs
|
||||||
*/
|
*/
|
||||||
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, maxTokens, endpoint, token) {
|
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, temperature, topP, maxTokens, endpoint, token) {
|
||||||
const messages = buildMessages(promptConfig, systemPrompt, prompt);
|
const messages = buildMessages(promptConfig, systemPrompt, prompt);
|
||||||
const responseFormat = buildResponseFormat(promptConfig);
|
const responseFormat = buildResponseFormat(promptConfig);
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
modelName,
|
modelName,
|
||||||
|
temperature,
|
||||||
|
topP,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
@@ -52572,10 +52587,8 @@ function loadPromptFile(filePath, templateVariables = {}) {
|
|||||||
throw new Error(`Prompt file not found: ${filePath}`);
|
throw new Error(`Prompt file not found: ${filePath}`);
|
||||||
}
|
}
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
// Apply template variable substitution
|
|
||||||
const processedContent = replaceTemplateVariables(fileContent, templateVariables);
|
|
||||||
try {
|
try {
|
||||||
const config = load(processedContent);
|
const config = load(fileContent);
|
||||||
if (!config.messages || !Array.isArray(config.messages)) {
|
if (!config.messages || !Array.isArray(config.messages)) {
|
||||||
throw new Error('Prompt file must contain a "messages" array');
|
throw new Error('Prompt file must contain a "messages" array');
|
||||||
}
|
}
|
||||||
@@ -52588,6 +52601,13 @@ function loadPromptFile(filePath, templateVariables = {}) {
|
|||||||
throw new Error(`Invalid message role: ${message.role}`);
|
throw new Error(`Invalid message role: ${message.role}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Prepare messages by replacing template variables with actual content
|
||||||
|
config.messages = config.messages.map(msg => {
|
||||||
|
return {
|
||||||
|
...msg,
|
||||||
|
content: replaceTemplateVariables(msg.content, templateVariables),
|
||||||
|
};
|
||||||
|
});
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -52607,9 +52627,6 @@ function isPromptYamlFile(filePath) {
|
|||||||
* @returns Resolves when the action is complete.
|
* @returns Resolves when the action is complete.
|
||||||
*/
|
*/
|
||||||
async function run() {
|
async function run() {
|
||||||
let responseFile = null;
|
|
||||||
// Set up graceful cleanup for temporary files on process exit
|
|
||||||
tmpExports.setGracefulCleanup();
|
|
||||||
try {
|
try {
|
||||||
const promptFilePath = coreExports.getInput('prompt-file');
|
const promptFilePath = coreExports.getInput('prompt-file');
|
||||||
const inputVariables = coreExports.getInput('input');
|
const inputVariables = coreExports.getInput('input');
|
||||||
@@ -52635,20 +52652,24 @@ async function run() {
|
|||||||
}
|
}
|
||||||
// Get common parameters
|
// Get common parameters
|
||||||
const modelName = promptConfig?.model || coreExports.getInput('model');
|
const modelName = promptConfig?.model || coreExports.getInput('model');
|
||||||
const maxTokens = parseInt(coreExports.getInput('max-tokens'), 10);
|
let maxTokens = promptConfig?.modelParameters?.maxTokens ?? coreExports.getInput('max-tokens');
|
||||||
|
if (typeof maxTokens === 'string') {
|
||||||
|
maxTokens = parseInt(maxTokens, 10);
|
||||||
|
}
|
||||||
const token = process.env['GITHUB_TOKEN'] || coreExports.getInput('token');
|
const token = process.env['GITHUB_TOKEN'] || coreExports.getInput('token');
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
throw new Error('GITHUB_TOKEN is not set');
|
throw new Error('GITHUB_TOKEN is not set');
|
||||||
}
|
}
|
||||||
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
|
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
|
||||||
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
|
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
|
||||||
|
const githubMcpToolsets = coreExports.getInput('github-mcp-toolsets');
|
||||||
const endpoint = coreExports.getInput('endpoint');
|
const endpoint = coreExports.getInput('endpoint');
|
||||||
// Build the inference request with pre-processed messages and response format
|
// Build the inference request with pre-processed messages and response format
|
||||||
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, maxTokens, endpoint, token);
|
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token);
|
||||||
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
|
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
|
||||||
let modelResponse = null;
|
let modelResponse = null;
|
||||||
if (enableMcp) {
|
if (enableMcp) {
|
||||||
const mcpClient = await connectToGitHubMCP(githubMcpToken);
|
const mcpClient = await connectToGitHubMCP(githubMcpToken, githubMcpToolsets);
|
||||||
if (mcpClient) {
|
if (mcpClient) {
|
||||||
modelResponse = await mcpInference(inferenceRequest, mcpClient);
|
modelResponse = await mcpInference(inferenceRequest, mcpClient);
|
||||||
}
|
}
|
||||||
@@ -52661,10 +52682,13 @@ async function run() {
|
|||||||
modelResponse = await simpleInference(inferenceRequest);
|
modelResponse = await simpleInference(inferenceRequest);
|
||||||
}
|
}
|
||||||
coreExports.setOutput('response', modelResponse || '');
|
coreExports.setOutput('response', modelResponse || '');
|
||||||
// Create a secure temporary file instead of using the temp directory directly
|
// Create a temporary file for the response that persists for downstream steps.
|
||||||
responseFile = tmpExports.fileSync({
|
// We use keep: true to prevent automatic cleanup - the file will be cleaned up
|
||||||
|
// by the runner when the job completes.
|
||||||
|
const responseFile = tmpExports.fileSync({
|
||||||
prefix: 'modelResponse-',
|
prefix: 'modelResponse-',
|
||||||
postfix: '.txt',
|
postfix: '.txt',
|
||||||
|
keep: true,
|
||||||
});
|
});
|
||||||
coreExports.setOutput('response-file', responseFile.name);
|
coreExports.setOutput('response-file', responseFile.name);
|
||||||
if (modelResponse && modelResponse !== '') {
|
if (modelResponse && modelResponse !== '') {
|
||||||
@@ -52681,18 +52705,6 @@ async function run() {
|
|||||||
// Force exit to prevent hanging on open connections
|
// Force exit to prevent hanging on open connections
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
// Explicit cleanup of temporary file if it was created
|
|
||||||
if (responseFile) {
|
|
||||||
try {
|
|
||||||
responseFile.removeCallback();
|
|
||||||
}
|
|
||||||
catch (cleanupError) {
|
|
||||||
// Log cleanup errors but don't fail the action
|
|
||||||
coreExports.warning(`Failed to cleanup temporary file: ${cleanupError}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Force exit to prevent hanging on open connections
|
// Force exit to prevent hanging on open connections
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -19,7 +19,7 @@ const compat = new FlatCompat({
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules'],
|
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules', 'script/**'],
|
||||||
},
|
},
|
||||||
...compat.extends(
|
...compat.extends(
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
|
|||||||
Generated
+23
-20
@@ -5168,11 +5168,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.4.6",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"picomatch": "^3 || ^4"
|
"picomatch": "^3 || ^4"
|
||||||
},
|
},
|
||||||
@@ -5458,9 +5461,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7852,9 +7855,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
@@ -8906,14 +8909,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.2"
|
"picomatch": "^4.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -9319,18 +9322,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.6",
|
"version": "7.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||||
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.6",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.40.0",
|
"rollup": "^4.43.0",
|
||||||
"tinyglobby": "^0.2.14"
|
"tinyglobby": "^0.2.15"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* A simple mock OpenAI-compatible inference server for CI testing.
|
||||||
|
* This returns predictable responses without needing real API credentials.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http from 'http'
|
||||||
|
|
||||||
|
const PORT = process.env.MOCK_SERVER_PORT || 3456
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
let body = ''
|
||||||
|
|
||||||
|
req.on('data', chunk => {
|
||||||
|
body += chunk.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
console.log(`[Mock Server] ${req.method} ${req.url}`)
|
||||||
|
|
||||||
|
// Handle chat completions endpoint
|
||||||
|
if (req.url === '/chat/completions' && req.method === 'POST') {
|
||||||
|
const request = JSON.parse(body)
|
||||||
|
const userMessage = request.messages?.find(m => m.role === 'user')?.content || 'No prompt'
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
id: 'mock-completion-id',
|
||||||
|
object: 'chat.completion',
|
||||||
|
created: Date.now(),
|
||||||
|
model: request.model || 'mock-model',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: `Mock response to: "${userMessage.slice(0, 50)}..."`,
|
||||||
|
},
|
||||||
|
finish_reason: 'stop',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 10,
|
||||||
|
completion_tokens: 20,
|
||||||
|
total_tokens: 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {'Content-Type': 'application/json'})
|
||||||
|
res.end(JSON.stringify(response))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
if (req.url === '/health' || req.url === '/') {
|
||||||
|
res.writeHead(200, {'Content-Type': 'application/json'})
|
||||||
|
res.end(JSON.stringify({status: 'ok'}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404 for unknown routes
|
||||||
|
res.writeHead(404, {'Content-Type': 'application/json'})
|
||||||
|
res.end(JSON.stringify({error: 'Not found'}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`[Mock Server] Listening on http://localhost:${PORT}`)
|
||||||
|
console.log('[Mock Server] Endpoints:')
|
||||||
|
console.log(' POST /chat/completions - Mock chat completion')
|
||||||
|
console.log(' GET /health - Health check')
|
||||||
|
})
|
||||||
@@ -82,6 +82,8 @@ export function buildInferenceRequest(
|
|||||||
systemPrompt: string | undefined,
|
systemPrompt: string | undefined,
|
||||||
prompt: string | undefined,
|
prompt: string | undefined,
|
||||||
modelName: string,
|
modelName: string,
|
||||||
|
temperature: number | undefined,
|
||||||
|
topP: number | undefined,
|
||||||
maxTokens: number,
|
maxTokens: number,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
token: string,
|
token: string,
|
||||||
@@ -92,6 +94,8 @@ export function buildInferenceRequest(
|
|||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
modelName,
|
modelName,
|
||||||
|
temperature,
|
||||||
|
topP,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export interface InferenceRequest {
|
|||||||
maxTokens: number
|
maxTokens: number
|
||||||
endpoint: string
|
endpoint: string
|
||||||
token: string
|
token: string
|
||||||
|
temperature?: number
|
||||||
|
topP?: number
|
||||||
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
|
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +47,8 @@ export async function simpleInference(request: InferenceRequest): Promise<string
|
|||||||
messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add response format if specified
|
// Add response format if specified
|
||||||
@@ -90,6 +94,8 @@ export async function mcpInference(
|
|||||||
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||||
max_tokens: request.maxTokens,
|
max_tokens: request.maxTokens,
|
||||||
model: request.modelName,
|
model: request.modelName,
|
||||||
|
temperature: request.temperature,
|
||||||
|
top_p: request.topP,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
||||||
|
|||||||
+14
-19
@@ -18,11 +18,6 @@ import {
|
|||||||
* @returns Resolves when the action is complete.
|
* @returns Resolves when the action is complete.
|
||||||
*/
|
*/
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
let responseFile: tmp.FileResult | null = null
|
|
||||||
|
|
||||||
// Set up graceful cleanup for temporary files on process exit
|
|
||||||
tmp.setGracefulCleanup()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const promptFilePath = core.getInput('prompt-file')
|
const promptFilePath = core.getInput('prompt-file')
|
||||||
const inputVariables = core.getInput('input')
|
const inputVariables = core.getInput('input')
|
||||||
@@ -53,7 +48,11 @@ export async function run(): Promise<void> {
|
|||||||
|
|
||||||
// Get common parameters
|
// Get common parameters
|
||||||
const modelName = promptConfig?.model || core.getInput('model')
|
const modelName = promptConfig?.model || core.getInput('model')
|
||||||
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
|
let maxTokens = promptConfig?.modelParameters?.maxTokens ?? core.getInput('max-tokens')
|
||||||
|
|
||||||
|
if (typeof maxTokens === 'string') {
|
||||||
|
maxTokens = parseInt(maxTokens, 10)
|
||||||
|
}
|
||||||
|
|
||||||
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
|
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
@@ -62,6 +61,7 @@ export async function run(): Promise<void> {
|
|||||||
|
|
||||||
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
|
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
|
||||||
const githubMcpToken = core.getInput('github-mcp-token') || token
|
const githubMcpToken = core.getInput('github-mcp-token') || token
|
||||||
|
const githubMcpToolsets = core.getInput('github-mcp-toolsets')
|
||||||
|
|
||||||
const endpoint = core.getInput('endpoint')
|
const endpoint = core.getInput('endpoint')
|
||||||
|
|
||||||
@@ -71,6 +71,8 @@ export async function run(): Promise<void> {
|
|||||||
systemPrompt,
|
systemPrompt,
|
||||||
prompt,
|
prompt,
|
||||||
modelName,
|
modelName,
|
||||||
|
promptConfig?.modelParameters?.temperature,
|
||||||
|
promptConfig?.modelParameters?.topP,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
@@ -81,7 +83,7 @@ export async function run(): Promise<void> {
|
|||||||
let modelResponse: string | null = null
|
let modelResponse: string | null = null
|
||||||
|
|
||||||
if (enableMcp) {
|
if (enableMcp) {
|
||||||
const mcpClient = await connectToGitHubMCP(githubMcpToken)
|
const mcpClient = await connectToGitHubMCP(githubMcpToken, githubMcpToolsets)
|
||||||
|
|
||||||
if (mcpClient) {
|
if (mcpClient) {
|
||||||
modelResponse = await mcpInference(inferenceRequest, mcpClient)
|
modelResponse = await mcpInference(inferenceRequest, mcpClient)
|
||||||
@@ -95,10 +97,13 @@ export async function run(): Promise<void> {
|
|||||||
|
|
||||||
core.setOutput('response', modelResponse || '')
|
core.setOutput('response', modelResponse || '')
|
||||||
|
|
||||||
// Create a secure temporary file instead of using the temp directory directly
|
// Create a temporary file for the response that persists for downstream steps.
|
||||||
responseFile = tmp.fileSync({
|
// We use keep: true to prevent automatic cleanup - the file will be cleaned up
|
||||||
|
// by the runner when the job completes.
|
||||||
|
const responseFile = tmp.fileSync({
|
||||||
prefix: 'modelResponse-',
|
prefix: 'modelResponse-',
|
||||||
postfix: '.txt',
|
postfix: '.txt',
|
||||||
|
keep: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
core.setOutput('response-file', responseFile.name)
|
core.setOutput('response-file', responseFile.name)
|
||||||
@@ -114,16 +119,6 @@ export async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
// Force exit to prevent hanging on open connections
|
// Force exit to prevent hanging on open connections
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
} finally {
|
|
||||||
// Explicit cleanup of temporary file if it was created
|
|
||||||
if (responseFile) {
|
|
||||||
try {
|
|
||||||
responseFile.removeCallback()
|
|
||||||
} catch (cleanupError) {
|
|
||||||
// Log cleanup errors but don't fail the action
|
|
||||||
core.warning(`Failed to cleanup temporary file: ${cleanupError}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force exit to prevent hanging on open connections
|
// Force exit to prevent hanging on open connections
|
||||||
|
|||||||
+15
-5
@@ -35,17 +35,27 @@ export interface GitHubMCPClient {
|
|||||||
/**
|
/**
|
||||||
* Connect to the GitHub MCP server and retrieve available tools
|
* Connect to the GitHub MCP server and retrieve available tools
|
||||||
*/
|
*/
|
||||||
export async function connectToGitHubMCP(token: string): Promise<GitHubMCPClient | null> {
|
export async function connectToGitHubMCP(token: string, toolsets?: string): Promise<GitHubMCPClient | null> {
|
||||||
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/'
|
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/'
|
||||||
|
|
||||||
core.info('Connecting to GitHub MCP server...')
|
core.info('Connecting to GitHub MCP server...')
|
||||||
|
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(githubMcpUrl), {
|
const headers: Record<string, string> = {
|
||||||
requestInit: {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'X-MCP-Readonly': 'true',
|
'X-MCP-Readonly': 'true',
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// Add toolsets header if specified
|
||||||
|
if (toolsets && toolsets.trim() !== '') {
|
||||||
|
headers['X-MCP-Toolsets'] = toolsets
|
||||||
|
core.info(`Using GitHub MCP toolsets: ${toolsets}`)
|
||||||
|
} else {
|
||||||
|
core.info('Using default GitHub MCP toolsets')
|
||||||
|
}
|
||||||
|
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(githubMcpUrl), {
|
||||||
|
requestInit: {
|
||||||
|
headers,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+16
-4
@@ -7,9 +7,16 @@ export interface PromptMessage {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModelParameters {
|
||||||
|
maxTokens?: number
|
||||||
|
temperature?: number
|
||||||
|
topP?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface PromptConfig {
|
export interface PromptConfig {
|
||||||
messages: PromptMessage[]
|
messages: PromptMessage[]
|
||||||
model?: string
|
model?: string
|
||||||
|
modelParameters?: ModelParameters
|
||||||
responseFormat?: 'text' | 'json_schema'
|
responseFormat?: 'text' | 'json_schema'
|
||||||
jsonSchema?: string
|
jsonSchema?: string
|
||||||
}
|
}
|
||||||
@@ -101,11 +108,8 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
|
|||||||
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
|
||||||
// Apply template variable substitution
|
|
||||||
const processedContent = replaceTemplateVariables(fileContent, templateVariables)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = yaml.load(processedContent) as PromptConfig
|
const config = yaml.load(fileContent) as PromptConfig
|
||||||
|
|
||||||
if (!config.messages || !Array.isArray(config.messages)) {
|
if (!config.messages || !Array.isArray(config.messages)) {
|
||||||
throw new Error('Prompt file must contain a "messages" array')
|
throw new Error('Prompt file must contain a "messages" array')
|
||||||
@@ -121,6 +125,14 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare messages by replacing template variables with actual content
|
||||||
|
config.messages = config.messages.map(msg => {
|
||||||
|
return {
|
||||||
|
...msg,
|
||||||
|
content: replaceTemplateVariables(msg.content, templateVariables),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return config
|
return config
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user