Merge branch 'main' into feature/pass-toolsets
This commit is contained in:
75
.github/workflows/ci.yml
vendored
75
.github/workflows/ci.yml
vendored
@@ -56,20 +56,51 @@ jobs:
|
|||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v6
|
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
|
||||||
@@ -79,6 +110,25 @@ jobs:
|
|||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v6
|
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
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -24,7 +24,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Test Local Action
|
- name: Test Local Action
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v2
|
uses: actions/ai-inference@v1
|
||||||
with:
|
with:
|
||||||
prompt: 'Hello!'
|
prompt: 'Hello!'
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ supports both plain text files and structured `.prompt.yml` files:
|
|||||||
steps:
|
steps:
|
||||||
- name: Run AI Inference with Text File
|
- name: Run AI Inference with Text File
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v2
|
uses: actions/ai-inference@v1
|
||||||
with:
|
with:
|
||||||
prompt-file: './path/to/prompt.txt'
|
prompt-file: './path/to/prompt.txt'
|
||||||
```
|
```
|
||||||
@@ -56,7 +56,7 @@ support templating, custom models, and JSON schema responses:
|
|||||||
steps:
|
steps:
|
||||||
- name: Run AI Inference with Prompt YAML
|
- name: Run AI Inference with Prompt YAML
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v2
|
uses: actions/ai-inference@v1
|
||||||
with:
|
with:
|
||||||
prompt-file: './.github/prompts/sample.prompt.yml'
|
prompt-file: './.github/prompts/sample.prompt.yml'
|
||||||
input: |
|
input: |
|
||||||
@@ -132,7 +132,7 @@ of an inline system prompt:
|
|||||||
steps:
|
steps:
|
||||||
- name: Run AI Inference with System Prompt File
|
- name: Run AI Inference with System Prompt File
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v2
|
uses: actions/ai-inference@v1
|
||||||
with:
|
with:
|
||||||
prompt: 'Hello!'
|
prompt: 'Hello!'
|
||||||
system-prompt-file: './path/to/system-prompt.txt'
|
system-prompt-file: './path/to/system-prompt.txt'
|
||||||
@@ -146,7 +146,7 @@ This can be useful when model response exceeds actions output limit
|
|||||||
steps:
|
steps:
|
||||||
- name: Test Local Action
|
- name: Test Local Action
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v2
|
uses: actions/ai-inference@v1
|
||||||
with:
|
with:
|
||||||
prompt: 'Hello!'
|
prompt: 'Hello!'
|
||||||
|
|
||||||
@@ -162,18 +162,28 @@ 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:
|
||||||
- name: AI Inference with GitHub Tools
|
- name: AI Inference with GitHub Tools
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v2
|
uses: actions/ai-inference@v1.2
|
||||||
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
|
||||||
@@ -183,12 +193,12 @@ and the GitHub MCP server:
|
|||||||
steps:
|
steps:
|
||||||
- name: AI Inference with Separate MCP Token
|
- name: AI Inference with Separate MCP Token
|
||||||
id: inference
|
id: inference
|
||||||
uses: actions/ai-inference@v2
|
uses: actions/ai-inference@v1.2
|
||||||
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.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
|
#### Configuring GitHub MCP Toolsets
|
||||||
@@ -218,21 +228,20 @@ perform actions like searching issues and PRs.
|
|||||||
Various inputs are defined in [`action.yml`](action.yml) to let you configure
|
Various inputs are defined in [`action.yml`](action.yml) to let you configure
|
||||||
the action:
|
the action:
|
||||||
|
|
||||||
| Name | Description | Default |
|
| Name | Description | Default |
|
||||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||||
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
||||||
| `prompt` | The prompt to send to the model | N/A |
|
| `prompt` | The prompt to send to the model | N/A |
|
||||||
| `prompt-file` | Path to a file containing the prompt (supports .txt and .prompt.yml formats). If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
|
| `prompt-file` | Path to a file containing the prompt (supports .txt and .prompt.yml formats). If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
|
||||||
| `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` |
|
| `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` |
|
||||||
| `file_input` | Template variables in YAML where values are file paths. The file contents are read and used for templating | `""` |
|
| `file_input` | Template variables in YAML where values are file paths. The file contents are read and used for templating | `""` |
|
||||||
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
|
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
|
||||||
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
|
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
|
||||||
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |
|
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |
|
||||||
| `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 in order for MCP to work | `""` |
|
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). | `""` |
|
||||||
| `github-mcp-toolsets` | Comma-separated list of toolsets to enable for GitHub MCP (e.g., `repos,issues,pull_requests,actions`). Use `all` for all toolsets or `default` for the default set | `""` |
|
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
22
dist/index.js
generated
vendored
22
dist/index.js
generated
vendored
@@ -52627,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');
|
||||||
@@ -52685,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 !== '') {
|
||||||
@@ -52705,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);
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -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',
|
||||||
|
|||||||
71
script/mock-inference-server.mjs
Normal file
71
script/mock-inference-server.mjs
Normal 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')
|
||||||
|
})
|
||||||
22
src/main.ts
22
src/main.ts
@@ -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')
|
||||||
@@ -102,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)
|
||||||
@@ -121,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
|
||||||
|
|||||||
Reference in New Issue
Block a user