Compare commits

..

4 Commits

Author SHA1 Message Date
Sean Goedecke 850c05ea4d dist 2025-04-07 22:52:09 +00:00
Sean Goedecke 701473dc11 use publisher 2025-04-07 22:43:25 +00:00
Sean Goedecke 786ceefcbc more logging 2025-04-07 22:31:16 +00:00
Sean Goedecke cb41259270 add logging 2025-04-07 22:26:14 +00:00
17 changed files with 3742 additions and 6850 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ ACTIONS_STEP_DEBUG=true
# GitHub Actions inputs should follow `INPUT_<name>` format (case-sensitive).
# Hyphens should not be converted to underscores!
INPUT_PROMPT=hello
INPUT_prompt=hello
INPUT_token=
# GitHub Actions default environment variables. These are set for every run of a
# workflow and can be used in your actions. Setting the value here will override
-30
View File
@@ -67,33 +67,3 @@ jobs:
- name: Print Output
id: output
run: echo "${{ steps.test-action.outputs.response }}"
test-action-prompt-file:
name: GitHub Actions Test with Prompt File
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Create Prompt File
run: echo "hello" > prompt.txt
- name: Create System Prompt File
run:
echo "You are a helpful AI assistant for testing." > system-prompt.txt
- name: Test Local Action with Prompt File
id: test-action-prompt-file
uses: ./
with:
prompt-file: prompt.txt
system-prompt-file: system-prompt.txt
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Print Output
run: |
echo "Response saved to: ${{ steps.test-action-prompt-file.outputs.response-file }}"
cat "${{ steps.test-action-prompt-file.outputs.response-file }}"
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- name: Lint Codebase
id: super-linter
uses: super-linter/super-linter/slim@12150456a73e248bdc94d0794898f94e23127c88
uses: super-linter/super-linter/slim@v7
env:
DEFAULT_BRANCH: main
FILTER_REGEX_EXCLUDE: dist/**/*
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: undici
version: 5.29.0
version: 5.28.5
type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org
+1 -1
View File
@@ -4,4 +4,4 @@
############################################################################
# Default owners, unless a later match takes precedence.
* @actions/models
* @actions/actions-oss-maintainers
+1 -1
View File
@@ -46,7 +46,7 @@ avoid having to include the `node_modules/` directory in the repository.
1. Make your change, add tests, and make sure the tests still pass:
`npm run test`
1. Make sure your code is correctly formatted: `npm run format`
1. Update `dist/index.js` using `npm run bundle`. This creates a single
1. Update `dist/index.js` using `npm run build`. This creates a single
JavaScript file that is used as an entrypoint for the action
1. Push to your fork and [submit a pull request][pr]
1. Pat yourself on the back and wait for your pull request to be reviewed and
+15 -77
View File
@@ -7,65 +7,12 @@
[![Coverage](./badges/coverage.svg)](./badges/coverage.svg)
Use AI models from [GitHub Models](https://github.com/marketplace/models) in
your workflows.
your actions.
## Usage
Create a workflow to use the AI inference action:
```yaml
name: 'AI inference'
on: workflow_dispatch
jobs:
inference:
permissions:
models: read
runs-on: ubuntu-latest
steps:
- name: Test Local Action
id: inference
uses: actions/ai-inference@v1
with:
prompt: 'Hello!'
- name: Print Output
id: output
run: echo "${{ steps.inference.outputs.response }}"
```
### Using a prompt file
You can also provide a prompt file instead of an inline prompt:
```yaml
steps:
- name: Run AI Inference with Prompt File
id: inference
uses: actions/ai-inference@v1
with:
prompt-file: './path/to/prompt.txt'
```
### Using a system prompt file
In addition to the regular prompt, you can provide a system prompt file instead
of an inline system prompt:
```yaml
steps:
- name: Run AI Inference with System Prompt File
id: inference
uses: actions/ai-inference@v1
with:
prompt: 'Hello!'
system-prompt-file: './path/to/system-prompt.txt'
```
### Read output from file instead of output
This can be useful when model response exceeds actions output limit
```yaml
steps:
- name: Test Local Action
@@ -74,10 +21,9 @@ steps:
with:
prompt: 'Hello!'
- name: Use Response File
run: |
echo "Response saved to: ${{ steps.inference.outputs.response-file }}"
cat "${{ steps.inference.outputs.response-file }}"
- name: Print Output
id: output
run: echo "${{ steps.test-action.outputs.response }}"
```
## Inputs
@@ -85,25 +31,22 @@ steps:
Various inputs are defined in [`action.yml`](action.yml) to let you configure
the action:
| Name | Description | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
| `prompt` | The prompt to send to the model | N/A |
| `prompt-file` | Path to a file containing the prompt. If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
| `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 | `""` |
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `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` |
| `max-tokens` | The max number of tokens to generate | 200 |
| Name | Description | Default |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
| `prompt` | The prompt to send to the model | N/A |
| `system-prompt` | The system prompt to send to the model | `""` |
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `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` |
| `max-tokens` | The max number of tokens to generate | 200 |
## Outputs
The AI inference action provides the following outputs:
| Name | Description |
| --------------- | ----------------------------------------------------------------------- |
| `response` | The response from the model |
| `response-file` | The file path where the response is saved (useful for larger responses) |
| Name | Description |
| ---------- | --------------------------- |
| `response` | The response from the model |
## Required Permissions
@@ -145,11 +88,6 @@ following steps:
to create a new release in GitHub so users can easily reference the new tags
in their workflows.
## License
This project is licensed under the terms of the MIT open source license. Please
refer to [MIT](./LICENSE.txt) for the full terms.
## Contributions
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).
-39
View File
@@ -1,39 +0,0 @@
# Security
GitHub takes the security of our software products and services seriously,
including all of the open source code repositories managed through our GitHub
organizations, such as [GitHub](https://github.com/GitHub).
Even though
[open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope)
and therefore not eligible for bounty rewards, we will ensure that your finding
gets passed along to the appropriate maintainers for remediation.
## Reporting Security Issues
If you believe you have found a security vulnerability in any GitHub-owned
repository, please report it to us through coordinated disclosure.
**Please do not report security vulnerabilities through public GitHub issues,
discussions, or pull requests.**
Instead, please send an email to opensource-security[@]github.com.
Please include as much of the information listed below as you can to help us
better understand and resolve the issue:
- The type of issue (e.g., buffer overflow, SQL injection, or cross-site
scripting)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
## Policy
See
[GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms)
-17
View File
@@ -1,17 +0,0 @@
# Support
## How to file issues and get help
This project uses GitHub issues to track bugs and feature requests. Please
search the existing issues before filing new issues to avoid duplicates. For new
issues, file your bug or feature request as a new issue.
For help or questions about using this project, please file an issue.
This project is under active development and maintained by GitHub staff and the
community. We will do our best to respond to support, feature requests, and
community questions in a timely manner.
## GitHub Support Policy
Support for this project is limited to the resources listed above.
+21 -328
View File
@@ -7,6 +7,7 @@
*/
import { jest } from '@jest/globals'
import * as core from '../__fixtures__/core.js'
const mockPost = jest.fn().mockImplementation(() => ({
body: {
choices: [
@@ -28,81 +29,6 @@ jest.unstable_mockModule('@azure-rest/ai-inference', () => ({
isUnexpected: jest.fn(() => false)
}))
// Default to throwing errors to catch unexpected calls
const mockExistsSync = jest.fn().mockImplementation(() => {
throw new Error(
'Unexpected call to existsSync - test should override this implementation'
)
})
const mockReadFileSync = jest.fn().mockImplementation(() => {
throw new Error(
'Unexpected call to readFileSync - test should override this implementation'
)
})
/**
* Helper function to mock file system operations for one or more files
* @param fileContents - Object mapping file paths to their contents
* @param nonExistentFiles - Array of file paths that should be treated as non-existent
*/
function mockFileContent(
fileContents: Record<string, string> = {},
nonExistentFiles: string[] = []
): void {
// Mock existsSync to return true for files that exist, false for those that don't
mockExistsSync.mockImplementation((...args: unknown[]): boolean => {
const [path] = args as [string]
if (nonExistentFiles.includes(path)) {
return false
}
return path in fileContents || true
})
// Mock readFileSync to return the content for known files
mockReadFileSync.mockImplementation((...args: unknown[]): string => {
const [path, options] = args as [string, BufferEncoding]
if (options === 'utf-8' && path in fileContents) {
return fileContents[path]
}
throw new Error(`Unexpected file read: ${path}`)
})
}
/**
* Helper function to mock action inputs
* @param inputs - Object mapping input names to their values
*/
function mockInputs(inputs: Record<string, string> = {}): void {
// Default values that are applied unless overridden
const defaultInputs: Record<string, string> = {
token: 'fake-token'
}
// Combine defaults with user-provided inputs
const allInputs: Record<string, string> = { ...defaultInputs, ...inputs }
core.getInput.mockImplementation((name: string) => {
return allInputs[name] || ''
})
}
/**
* Helper function to verify common response assertions
*/
function verifyStandardResponse(): void {
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'response', 'Hello, user!')
expect(core.setOutput).toHaveBeenNthCalledWith(
2,
'response-file',
expect.stringContaining('modelResponse.txt')
)
}
jest.unstable_mockModule('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync
}))
jest.unstable_mockModule('@actions/core', () => core)
// The module being tested should be imported dynamically. This ensures that the
@@ -110,270 +36,37 @@ jest.unstable_mockModule('@actions/core', () => core)
const { run } = await import('../src/main.js')
describe('main.ts', () => {
// Reset all mocks before each test
beforeEach(() => {
jest.clearAllMocks()
// Set the action's inputs as return values from core.getInput().
core.getInput.mockImplementation((name) => {
if (name === 'prompt') return 'Hello, AI!'
if (name === 'system_prompt') return 'You are a test assistant.'
if (name === 'model_name') return 'gpt-4o'
return ''
})
})
afterEach(() => {
jest.resetAllMocks()
})
it('Sets the response output', async () => {
// Set the action's inputs as return values from core.getInput().
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.'
})
await run()
expect(core.setOutput).toHaveBeenCalled()
verifyStandardResponse()
expect(core.setOutput).toHaveBeenNthCalledWith(
1,
'response',
'Hello, user!'
)
})
it('Sets a failed status when no prompt is set', async () => {
// Clear the getInput mock and simulate no prompt or prompt-file input
mockInputs({
prompt: '',
'prompt-file': ''
})
it('Sets a failed status', async () => {
// Clear the getInput mock and return an empty prompt
core.getInput.mockClear().mockReturnValueOnce('')
await run()
// Verify that the action was marked as failed.
expect(core.setFailed).toHaveBeenNthCalledWith(
1,
'Neither prompt-file nor prompt was set'
)
})
it('uses prompt-file', async () => {
const promptFile = 'prompt.txt'
const promptContent = 'This is a prompt from a file'
// Set up mock to return specific content for the prompt file
mockFileContent({
[promptFile]: promptContent
})
// Set up input mocks
mockInputs({
'prompt-file': promptFile,
'system-prompt': 'You are a test assistant.'
})
await run()
expect(mockExistsSync).toHaveBeenCalledWith(promptFile)
expect(mockReadFileSync).toHaveBeenCalledWith(promptFile, 'utf-8')
verifyStandardResponse()
})
it('handles non-existent prompt-file with an error', async () => {
const promptFile = 'non-existent-prompt.txt'
// Mock the file not existing
mockFileContent({}, [promptFile])
// Set up input mocks
mockInputs({
'prompt-file': promptFile
})
await run()
// Verify that the error was correctly reported
expect(core.setFailed).toHaveBeenCalledWith(
`File for prompt-file was not found: ${promptFile}`
)
})
it('prefers prompt-file over prompt when both are provided', async () => {
const promptFile = 'prompt.txt'
const promptFileContent = 'This is a prompt from a file that should be used'
const promptString = 'This is a direct prompt that should be ignored'
// Set up mock to return specific content for the prompt file
mockFileContent({
[promptFile]: promptFileContent
})
// Set up input mocks
mockInputs({
prompt: promptString,
'prompt-file': promptFile,
'system-prompt': 'You are a test assistant.'
})
await run()
expect(mockExistsSync).toHaveBeenCalledWith(promptFile)
expect(mockReadFileSync).toHaveBeenCalledWith(promptFile, 'utf-8')
// Check that the post call was made with the prompt from the file, not the input parameter
expect(mockPost).toHaveBeenCalledWith({
body: {
messages: [
{
role: 'system',
content: expect.any(String)
},
{ role: 'user', content: promptFileContent } // Should use the file content, not the string input
],
max_tokens: expect.any(Number),
model: expect.any(String)
}
})
verifyStandardResponse()
})
it('uses system-prompt-file', async () => {
const systemPromptFile = 'system-prompt.txt'
const systemPromptContent =
'You are a specialized system assistant for testing'
// Set up mock to return specific content for the system prompt file
mockFileContent({
[systemPromptFile]: systemPromptContent
})
// Set up input mocks
mockInputs({
prompt: 'Hello, AI!',
'system-prompt-file': systemPromptFile
})
await run()
expect(mockExistsSync).toHaveBeenCalledWith(systemPromptFile)
expect(mockReadFileSync).toHaveBeenCalledWith(systemPromptFile, 'utf-8')
verifyStandardResponse()
})
it('handles non-existent system-prompt-file with an error', async () => {
const systemPromptFile = 'non-existent-system-prompt.txt'
// Mock the file not existing
mockFileContent({}, [systemPromptFile])
// Set up input mocks
mockInputs({
prompt: 'Hello, AI!',
'system-prompt-file': systemPromptFile
})
await run()
// Verify that the error was correctly reported
expect(core.setFailed).toHaveBeenCalledWith(
`File for system-prompt-file was not found: ${systemPromptFile}`
)
})
it('prefers system-prompt-file over system-prompt when both are provided', async () => {
const systemPromptFile = 'system-prompt.txt'
const systemPromptFileContent =
'You are a specialized system assistant from file'
const systemPromptString =
'You are a basic system assistant from input parameter'
// Set up mock to return specific content for the system prompt file
mockFileContent({
[systemPromptFile]: systemPromptFileContent
})
// Set up input mocks
mockInputs({
prompt: 'Hello, AI!',
'system-prompt-file': systemPromptFile,
'system-prompt': systemPromptString
})
await run()
expect(mockExistsSync).toHaveBeenCalledWith(systemPromptFile)
expect(mockReadFileSync).toHaveBeenCalledWith(systemPromptFile, 'utf-8')
// Check that the post call was made with the system prompt from the file, not the input parameter
expect(mockPost).toHaveBeenCalledWith({
body: {
messages: [
{
role: 'system',
content: systemPromptFileContent // Should use the file content, not the string input
},
{ role: 'user', content: 'Hello, AI!' }
],
max_tokens: expect.any(Number),
model: expect.any(String)
}
})
verifyStandardResponse()
})
it('uses both prompt-file and system-prompt-file together', async () => {
const promptFile = 'prompt.txt'
const promptContent = 'This is a prompt from a file'
const systemPromptFile = 'system-prompt.txt'
const systemPromptContent =
'You are a specialized system assistant from file'
// Set up mock to return specific content for both files
mockFileContent({
[promptFile]: promptContent,
[systemPromptFile]: systemPromptContent
})
// Set up input mocks
mockInputs({
'prompt-file': promptFile,
'system-prompt-file': systemPromptFile
})
await run()
expect(mockExistsSync).toHaveBeenCalledWith(promptFile)
expect(mockExistsSync).toHaveBeenCalledWith(systemPromptFile)
expect(mockReadFileSync).toHaveBeenCalledWith(promptFile, 'utf-8')
expect(mockReadFileSync).toHaveBeenCalledWith(systemPromptFile, 'utf-8')
// Check that the post call was made with both the prompt and system prompt from files
expect(mockPost).toHaveBeenCalledWith({
body: {
messages: [
{
role: 'system',
content: systemPromptContent
},
{ role: 'user', content: promptContent }
],
max_tokens: expect.any(Number),
model: expect.any(String)
}
})
verifyStandardResponse()
})
it('passes custom max-tokens parameter to the model', async () => {
const customMaxTokens = 500
mockInputs({
prompt: 'Hello, AI!',
'system-prompt': 'You are a test assistant.',
'max-tokens': customMaxTokens.toString()
})
await run()
// Check that the post call was made with the correct max_tokens parameter
expect(mockPost).toHaveBeenCalledWith({
body: {
messages: expect.any(Array),
max_tokens: customMaxTokens,
model: expect.any(String)
}
})
verifyStandardResponse()
expect(core.setFailed).toHaveBeenNthCalledWith(1, 'prompt is not set')
})
})
+2 -12
View File
@@ -4,18 +4,14 @@ author: 'GitHub'
# Add your action's branding here. This will appear on the GitHub Marketplace.
branding:
icon: 'message-square'
icon: 'play-circle'
color: red
# Define your inputs here.
inputs:
prompt:
description: The prompt for the model
required: false
default: ''
prompt-file:
description: Path to a file containing the prompt
required: false
required: true
default: ''
model:
description: The model to use
@@ -29,10 +25,6 @@ inputs:
description: The system prompt for the model
required: false
default: 'You are a helpful assistant'
system-prompt-file:
description: Path to a file containing the system prompt
required: false
default: ''
max-tokens:
description: The maximum number of tokens to generate
required: false
@@ -46,8 +38,6 @@ inputs:
outputs:
response:
description: The response from the model
response-file:
description: The file path where the response is saved
runs:
using: node20
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 84.21%"><title>Coverage: 84.21%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#dfb317"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">84.21%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">84.21%</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 77.27%"><title>Coverage: 77.27%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#e05d44"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">77.27%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">77.27%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Generated Vendored
+1751 -2583
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+1920 -3679
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,7 +1,7 @@
{
"name": "typescript-action",
"description": "GitHub Actions TypeScript template",
"version": "1.0.0",
"version": "0.0.0",
"author": "",
"type": "module",
"private": true,
@@ -32,7 +32,7 @@
"local-action": "npx @github/local-action . src/main.ts .env",
"package": "npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
"package:watch": "npm run package -- --watch",
"test": "npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
},
"license": "MIT",
@@ -43,30 +43,30 @@
"@azure-rest/ai-inference": "latest",
"@azure/core-auth": "latest",
"@azure/core-sse": "latest",
"@eslint/compat": "^1.2.9",
"@github/local-action": "^3.2.1",
"@eslint/compat": "^1.2.7",
"@github/local-action": "^3.1.3",
"@jest/globals": "^29.7.0",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-typescript": "^12.1.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.21",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.4.0",
"@types/node": "^20.17.28",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.0.2",
"eslint-import-resolver-typescript": "^4.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-prettier": "^5.2.5",
"jest": "^29.7.0",
"make-coverage-badge": "^1.2.0",
"prettier": "^3.5.3",
"prettier-eslint": "^16.4.2",
"rollup": "^4.41.1",
"ts-jest": "^29.3.4",
"prettier-eslint": "^16.3.0",
"rollup": "^4.38.0",
"ts-jest": "^29.3.0",
"ts-jest-resolver": "^2.0.1",
"typescript": "^5.8.3"
"typescript": "^5.8.2"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"
+8 -61
View File
@@ -1,40 +1,6 @@
import * as core from '@actions/core'
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
import { AzureKeyCredential } from '@azure/core-auth'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
const RESPONSE_FILE = 'modelResponse.txt'
/**
* Helper function to load content from a file or use fallback input
* @param filePathInput - Input name for the file path
* @param contentInput - Input name for the direct content
* @param defaultValue - Default value to use if neither file nor content is provided
* @returns The loaded content
*/
function loadContentFromFileOrInput(
filePathInput: string,
contentInput: string,
defaultValue?: string
): string {
const filePath = core.getInput(filePathInput)
const contentString = core.getInput(contentInput)
if (filePath !== undefined && filePath !== '') {
if (!fs.existsSync(filePath)) {
throw new Error(`File for ${filePathInput} was not found: ${filePath}`)
}
return fs.readFileSync(filePath, 'utf-8')
} else if (contentString !== undefined && contentString !== '') {
return contentString
} else if (defaultValue !== undefined) {
return defaultValue
} else {
throw new Error(`Neither ${filePathInput} nor ${contentInput} was set`)
}
}
/**
* The main function for the action.
@@ -43,16 +9,12 @@ function loadContentFromFileOrInput(
*/
export async function run(): Promise<void> {
try {
// Load prompt content - required
const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
// Load system prompt with default value
const systemPrompt = loadContentFromFileOrInput(
'system-prompt-file',
'system-prompt',
'You are a helpful assistant'
)
const prompt: string = core.getInput('prompt')
if (prompt === undefined || prompt === '') {
throw new Error('prompt is not set')
}
const systemPrompt: string = core.getInput('system-prompt')
const modelName: string = core.getInput('model')
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
@@ -60,12 +22,9 @@ export async function run(): Promise<void> {
if (token === undefined) {
throw new Error('GITHUB_TOKEN is not set')
}
const endpoint = core.getInput('endpoint')
const client = ModelClient(endpoint, new AzureKeyCredential(token), {
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
})
const client = ModelClient(endpoint, new AzureKeyCredential(token))
const response = await client.path('/chat/completions').post({
body: {
@@ -76,6 +35,8 @@ export async function run(): Promise<void> {
},
{ role: 'user', content: prompt }
],
temperature: 1.0,
top_p: 1.0,
max_tokens: maxTokens,
model: modelName
}
@@ -92,20 +53,11 @@ export async function run(): Promise<void> {
response.body
)
}
const modelResponse: string | null =
response.body.choices[0].message.content
// Set outputs for other workflow steps to use
core.setOutput('response', modelResponse || '')
// Save the response to a file in case the response overflow the output limit
const responseFilePath = path.join(tempDir(), RESPONSE_FILE)
core.setOutput('response-file', responseFilePath)
if (modelResponse && modelResponse !== '') {
fs.writeFileSync(responseFilePath, modelResponse, 'utf-8')
}
} catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error) {
@@ -115,8 +67,3 @@ export async function run(): Promise<void> {
}
}
}
function tempDir(): string {
const tempDirectory = process.env['RUNNER_TEMP'] || os.tmpdir()
return tempDirectory
}