34 Commits
v0 ... v1.1.0

Author SHA1 Message Date
Sean Goedecke
d645f067d8 Merge pull request #27 from mattleibow/dev/system-prompt-file
feat: Add system-prompt-file input for file-based system prompts
2025-05-27 09:48:56 +10:00
Matthew Leibowitz
9c57490bf1 regen 2025-05-27 01:40:03 +02:00
Matthew Leibowitz
aa31275bdc Merge remote-tracking branch 'upstream/main' into dev/system-prompt-file 2025-05-27 01:33:37 +02:00
Sean Goedecke
cacab0de8c Merge pull request #28 from actions/dependabot/npm_and_yarn/github/local-action-3.2.1
Bump @github/local-action from 2.2.1 to 3.2.1
2025-05-27 07:40:55 +10:00
dependabot[bot]
8562e77a99 Bump @github/local-action from 2.2.1 to 3.2.1
Bumps [@github/local-action](https://github.com/github/local-action) from 2.2.1 to 3.2.1.
- [Release notes](https://github.com/github/local-action/releases)
- [Changelog](https://github.com/github/local-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/local-action/compare/v2.2.1...v3.2.1)

---
updated-dependencies:
- dependency-name: "@github/local-action"
  dependency-version: 3.2.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 02:24:05 +00:00
Matthew Leibowitz
9aac9c75b3 Formatting 2025-05-26 04:03:09 +02:00
Matthew Leibowitz
eb37c9a493 Formatting 2025-05-26 03:46:39 +02:00
dependabot[bot]
7ee5d2347b Merge pull request #24 from actions/dependabot/github_actions/actions-minor-88e1b89f1e 2025-05-26 00:50:33 +00:00
dependabot[bot]
c9a9379c71 Bump super-linter/super-linter in the actions-minor group
Bumps the actions-minor group with 1 update: [super-linter/super-linter](https://github.com/super-linter/super-linter).


Updates `super-linter/super-linter` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/super-linter/super-linter/releases)
- [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md)
- [Commits](4e8a7c2bf1...12150456a7)

---
updated-dependencies:
- dependency-name: super-linter/super-linter
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 00:48:34 +00:00
Marais Rossouw
ad31e754e3 Merge pull request #25 from actions/mr/bump-versions
Bumps all package versions, and re-builds
2025-05-26 10:46:40 +10:00
Marais Rossouw
c4ce17bc84 chore: updates licenses 2025-05-26 00:35:07 +00:00
Marais Rossouw
c0259b3c7d chore: re-builds bundle 2025-05-26 00:34:45 +00:00
Marais Rossouw
989a68a941 chore: bumps all package versions 2025-05-26 00:34:16 +00:00
Matthew Leibowitz
3e924fe06b Refactor the prompt reading logic 2025-05-25 19:08:36 +02:00
Matthew Leibowitz
96e0fda3bb Improve the tests a bit
Removed the duplicate code and just use helper functions.
2025-05-24 04:07:29 +02:00
Matthew Leibowitz
91ba53d8b4 feat: Add system-prompt-file input for file-based system prompts
This enhancement adds the ability to load a system prompt from a file, similar to
the existing prompt-file functionality, providing more flexibility when working with
complex system prompts.

Key changes:
- Added new `system-prompt-file` input to action.yml with proper description
- Updated main.ts implementation to handle file-based system prompts with:
  - File existence checking and appropriate error handling
  - Proper precedence (system-prompt-file takes priority over system-prompt)
  - Consistent error messages with existing prompt-file implementation

Test coverage added:
- Basic usage: Test verifies system-prompt-file loads content correctly
- Error handling: Test ensures proper errors when system-prompt-file doesn't exist
- Precedence: Test confirms system-prompt-file overrides system-prompt when both exist
- Integration: Test validates both prompt-file and system-prompt-file work together
- Max tokens: Test verifies custom token limits are properly passed to the model
- Testing infrastructure: Improved mock implementations that detect unexpected calls

Documentation:
- Updated README.md with system-prompt-file in inputs table
- Added dedicated usage example for system-prompt-file
- Fixed formatting in inputs table

CI/CD:
- Updated workflow to test system-prompt-file functionality with actual file content

This feature maintains backward compatibility while adding a useful option
for workflows that need to use more complex system prompts stored in files.
2025-05-24 03:50:15 +02:00
dependabot[bot]
f8ee4c952b Merge pull request #16 from actions/dependabot/npm_and_yarn/npm-development-43ab01df74 2025-04-23 08:27:57 +00:00
dependabot[bot]
d0b41e9e29 Bump the npm-development group across 1 directory with 14 updates
Bumps the npm-development group with 14 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@azure/core-sse](https://github.com/Azure/azure-sdk-for-js) | `2.1.3` | `2.2.0` |
| [@eslint/compat](https://github.com/eslint/rewrite) | `1.2.7` | `1.2.8` |
| [@github/local-action](https://github.com/github/local-action) | `3.1.4` | `3.2.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.17.28` | `20.17.30` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.28.0` | `8.30.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.28.0` | `8.30.1` |
| [eslint](https://github.com/eslint/eslint) | `9.23.0` | `9.24.0` |
| [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) | `10.1.1` | `10.1.2` |
| [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) | `4.3.1` | `4.3.2` |
| [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) | `5.2.5` | `5.2.6` |
| [prettier-eslint](https://github.com/prettier/prettier-eslint) | `16.3.0` | `16.3.2` |
| [rollup](https://github.com/rollup/rollup) | `4.38.0` | `4.40.0` |
| [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.3.0` | `29.3.2` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.8.2` | `5.8.3` |



Updates `@azure/core-sse` from 2.1.3 to 2.2.0
- [Release notes](https://github.com/Azure/azure-sdk-for-js/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Changelog-for-next-generation.md)
- [Commits](https://github.com/Azure/azure-sdk-for-js/compare/@azure/core-sse_2.1.3...@azure/core-sse_2.2.0)

Updates `@eslint/compat` from 1.2.7 to 1.2.8
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/release-please-config.json)
- [Commits](https://github.com/eslint/rewrite/compare/compat-v1.2.7...compat-v1.2.8)

Updates `@github/local-action` from 3.1.4 to 3.2.0
- [Release notes](https://github.com/github/local-action/releases)
- [Changelog](https://github.com/github/local-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/local-action/compare/v3.1.4...v3.2)

Updates `@types/node` from 20.17.28 to 20.17.30
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@typescript-eslint/eslint-plugin` from 8.28.0 to 8.30.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.30.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.28.0 to 8.30.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.30.1/packages/parser)

Updates `eslint` from 9.23.0 to 9.24.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.23.0...v9.24.0)

Updates `eslint-config-prettier` from 10.1.1 to 10.1.2
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v10.1.1...v10.1.2)

Updates `eslint-import-resolver-typescript` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v4.3.1...v4.3.2)

Updates `eslint-plugin-prettier` from 5.2.5 to 5.2.6
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.2.5...v5.2.6)

Updates `prettier-eslint` from 16.3.0 to 16.3.2
- [Release notes](https://github.com/prettier/prettier-eslint/releases)
- [Changelog](https://github.com/prettier/prettier-eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier-eslint/compare/v16.3.0...v16.3.2)

Updates `rollup` from 4.38.0 to 4.40.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.38.0...v4.40.0)

Updates `ts-jest` from 29.3.0 to 29.3.2
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.3.0...v29.3.2)

Updates `typescript` from 5.8.2 to 5.8.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.2...v5.8.3)

---
updated-dependencies:
- dependency-name: "@azure/core-sse"
  dependency-version: 2.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@eslint/compat"
  dependency-version: 1.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@github/local-action"
  dependency-version: 3.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@types/node"
  dependency-version: 20.17.30
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.30.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.30.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: eslint
  dependency-version: 9.24.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: eslint-config-prettier
  dependency-version: 10.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: eslint-import-resolver-typescript
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: prettier-eslint
  dependency-version: 16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: rollup
  dependency-version: 4.40.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: ts-jest
  dependency-version: 29.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: typescript
  dependency-version: 5.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 08:24:38 +00:00
dependabot[bot]
8eaf9b3bbc Merge pull request #13 from actions/dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.40.0 2025-04-23 08:21:41 +00:00
Marais Rossouw
d9d6269e33 Merge branch 'main' into dependabot/npm_and_yarn/rollup/rollup-linux-x64-gnu-4.40.0 2025-04-23 18:19:29 +10:00
Sean Goedecke
d043c3eaa1 Merge pull request #10 from actions/joshmgross/fix-readme
Clarify description in README
2025-04-22 08:38:45 +10:00
Sean Goedecke
5b3308935f Merge branch 'main' into joshmgross/fix-readme 2025-04-22 08:36:33 +10:00
dependabot[bot]
2d37c90e93 Bump @rollup/rollup-linux-x64-gnu from 4.39.0 to 4.40.0
Bumps [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) from 4.39.0 to 4.40.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.39.0...v4.40.0)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-17 21:12:36 +00:00
Aiqiao Yan
b72d8483c7 Merge pull request #15 from actions/aiqiaoy/read-prompt-from-file
read prompt from file and print output to file
2025-04-17 17:11:26 -04:00
Aiqiao Yan
2aea1d4fc8 add a ci test 2025-04-17 21:04:14 +00:00
Aiqiao Yan
1c557cdc25 react to feedback 2025-04-17 20:23:07 +00:00
Aiqiao Yan
f1591cfa68 add test 2025-04-17 20:13:47 +00:00
Aiqiao Yan
43f6a3831f read prompt from file and print output to file 2025-04-17 17:41:35 +00:00
Josh Gross
a2fd55fb87 Merge branch 'main' into joshmgross/fix-readme 2025-04-14 14:22:13 -04:00
Sean Goedecke
c7105a4c1e Merge pull request #11 from actions/sgoedecke/tweaks
Update readme and update/pin dependencies
2025-04-11 07:55:13 +10:00
Sean Goedecke
b75b177af3 update codeowners 2025-04-10 21:37:11 +00:00
Sean Goedecke
a3d57cc6dc Linting 2025-04-10 21:33:06 +00:00
Sean Goedecke
9239ab5d65 version to v1 2025-04-10 21:27:55 +00:00
Josh Gross
72f855ca83 Clarify description in README 2025-04-10 08:53:19 -04:00
14 changed files with 6732 additions and 3728 deletions

View File

@@ -67,3 +67,33 @@ 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 }}"

View File

@@ -38,7 +38,7 @@ jobs:
- name: Lint Codebase
id: super-linter
uses: super-linter/super-linter/slim@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2
uses: super-linter/super-linter/slim@12150456a73e248bdc94d0794898f94e23127c88
env:
DEFAULT_BRANCH: main
FILTER_REGEX_EXCLUDE: dist/**/*

View File

@@ -1,6 +1,6 @@
---
name: undici
version: 5.28.5
version: 5.29.0
type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org

View File

@@ -4,4 +4,4 @@
############################################################################
# Default owners, unless a later match takes precedence.
* @actions/actions-oss-maintainers
* @actions/models

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 build`. This creates a single
1. Update `dist/index.js` using `npm run bundle`. 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

View File

@@ -7,7 +7,7 @@
[![Coverage](./badges/coverage.svg)](./badges/coverage.svg)
Use AI models from [GitHub Models](https://github.com/marketplace/models) in
your actions.
your workflows.
## Usage
@@ -34,27 +34,76 @@ jobs:
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
id: inference
uses: actions/ai-inference@v1
with:
prompt: 'Hello!'
- name: Use Response File
run: |
echo "Response saved to: ${{ steps.inference.outputs.response-file }}"
cat "${{ steps.inference.outputs.response-file }}"
```
## Inputs
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 |
| `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 |
| 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 |
## Outputs
The AI inference action provides the following outputs:
| Name | Description |
| ---------- | --------------------------- |
| `response` | The response from the model |
| Name | Description |
| --------------- | ----------------------------------------------------------------------- |
| `response` | The response from the model |
| `response-file` | The file path where the response is saved (useful for larger responses) |
## Required Permissions
@@ -96,9 +145,10 @@ following steps:
to create a new release in GitHub so users can easily reference the new tags
in their workflows.
## License
## License
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE.txt) for the full terms.
This project is licensed under the terms of the MIT open source license. Please
refer to [MIT](./LICENSE.txt) for the full terms.
## Contributions

View File

@@ -7,7 +7,6 @@
*/
import { jest } from '@jest/globals'
import * as core from '../__fixtures__/core.js'
const mockPost = jest.fn().mockImplementation(() => ({
body: {
choices: [
@@ -29,6 +28,81 @@ 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
@@ -36,37 +110,270 @@ jest.unstable_mockModule('@actions/core', () => core)
const { run } = await import('../src/main.js')
describe('main.ts', () => {
// Reset all mocks before each test
beforeEach(() => {
// 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()
jest.clearAllMocks()
})
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).toHaveBeenNthCalledWith(
1,
'response',
'Hello, user!'
)
expect(core.setOutput).toHaveBeenCalled()
verifyStandardResponse()
})
it('Sets a failed status', async () => {
// Clear the getInput mock and return an empty prompt
core.getInput.mockClear().mockReturnValueOnce('')
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': ''
})
await run()
// Verify that the action was marked as failed.
expect(core.setFailed).toHaveBeenNthCalledWith(1, 'prompt is not set')
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()
})
})

View File

@@ -11,7 +11,11 @@ branding:
inputs:
prompt:
description: The prompt for the model
required: true
required: false
default: ''
prompt-file:
description: Path to a file containing the prompt
required: false
default: ''
model:
description: The model to use
@@ -25,6 +29,10 @@ 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
@@ -38,6 +46,8 @@ inputs:
outputs:
response:
description: The response from the model
response-file:
description: The file path where the response is saved
runs:
using: node20

View File

@@ -1 +1 @@
<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>
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

4328
dist/index.js generated vendored

File diff suppressed because it is too large Load Diff

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

5556
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "typescript-action",
"description": "GitHub Actions TypeScript template",
"version": "0.0.0",
"version": "1.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": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"test": "npx cross-env 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.7",
"@github/local-action": "^3.1.3",
"@eslint/compat": "^1.2.9",
"@github/local-action": "^3.2.1",
"@jest/globals": "^29.7.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-typescript": "^12.1.2",
"@types/jest": "^29.5.14",
"@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",
"@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",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-prettier": "^5.4.0",
"jest": "^29.7.0",
"make-coverage-badge": "^1.2.0",
"prettier": "^3.5.3",
"prettier-eslint": "^16.3.0",
"rollup": "^4.38.0",
"ts-jest": "^29.3.0",
"prettier-eslint": "^16.4.2",
"rollup": "^4.41.1",
"ts-jest": "^29.3.4",
"ts-jest-resolver": "^2.0.1",
"typescript": "^5.8.2"
"typescript": "^5.8.3"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*"

View File

@@ -1,6 +1,40 @@
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.
@@ -9,12 +43,16 @@ import { AzureKeyCredential } from '@azure/core-auth'
*/
export async function run(): Promise<void> {
try {
const prompt: string = core.getInput('prompt')
if (prompt === undefined || prompt === '') {
throw new Error('prompt is not set')
}
// 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 systemPrompt: string = core.getInput('system-prompt')
const modelName: string = core.getInput('model')
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
@@ -60,6 +98,14 @@ export async function run(): Promise<void> {
// 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) {
@@ -69,3 +115,8 @@ export async function run(): Promise<void> {
}
}
}
function tempDir(): string {
const tempDirectory = process.env['RUNNER_TEMP'] || os.tmpdir()
return tempDirectory
}