Merge branch 'main' into dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.52.5

This commit is contained in:
Sarah Vessels
2026-01-06 13:45:00 -06:00
committed by GitHub
21 changed files with 365 additions and 132 deletions
+3 -3
View File
@@ -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/
+75 -8
View File
@@ -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
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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
+34 -5
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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)
})
}) })
+34
View File
@@ -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', () => {
+4
View File
@@ -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:
Generated Vendored
+41 -29
View File
@@ -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);
} }
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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',
+23 -20
View File
@@ -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"
+71
View File
@@ -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')
})
+4
View File
@@ -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,
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'}`)