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 - name: Print Output
id: output id: output
run: echo "${{ steps.test-action.outputs.response }}" 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 - name: Lint Codebase
id: super-linter id: super-linter
uses: super-linter/super-linter/slim@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2 uses: super-linter/super-linter/slim@12150456a73e248bdc94d0794898f94e23127c88
env: env:
DEFAULT_BRANCH: main DEFAULT_BRANCH: main
FILTER_REGEX_EXCLUDE: dist/**/* FILTER_REGEX_EXCLUDE: dist/**/*

View File

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

View File

@@ -4,4 +4,4 @@
############################################################################ ############################################################################
# Default owners, unless a later match takes precedence. # 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: 1. Make your change, add tests, and make sure the tests still pass:
`npm run test` `npm run test`
1. Make sure your code is correctly formatted: `npm run format` 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 JavaScript file that is used as an entrypoint for the action
1. Push to your fork and [submit a pull request][pr] 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 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) [![Coverage](./badges/coverage.svg)](./badges/coverage.svg)
Use AI models from [GitHub Models](https://github.com/marketplace/models) in Use AI models from [GitHub Models](https://github.com/marketplace/models) in
your actions. your workflows.
## Usage ## Usage
@@ -34,27 +34,76 @@ jobs:
run: echo "${{ steps.inference.outputs.response }}" 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 ## Inputs
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 |
| `system-prompt` | The system prompt to send to the model | `""` | | `prompt-file` | Path to a file containing the prompt. If both `prompt` and `prompt-file` are provided, `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` | | `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
| `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` | | `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 | `""` |
| `max-tokens` | The max number of tokens to generate | 200 | | `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 ## Outputs
The AI inference action provides the following outputs: The AI inference action provides the following outputs:
| Name | Description | | Name | Description |
| ---------- | --------------------------- | | --------------- | ----------------------------------------------------------------------- |
| `response` | The response from the model | | `response` | The response from the model |
| `response-file` | The file path where the response is saved (useful for larger responses) |
## Required Permissions ## Required Permissions
@@ -96,9 +145,10 @@ following steps:
to create a new release in GitHub so users can easily reference the new tags to create a new release in GitHub so users can easily reference the new tags
in their workflows. 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 ## Contributions

View File

@@ -7,7 +7,6 @@
*/ */
import { jest } from '@jest/globals' import { jest } from '@jest/globals'
import * as core from '../__fixtures__/core.js' import * as core from '../__fixtures__/core.js'
const mockPost = jest.fn().mockImplementation(() => ({ const mockPost = jest.fn().mockImplementation(() => ({
body: { body: {
choices: [ choices: [
@@ -29,6 +28,81 @@ jest.unstable_mockModule('@azure-rest/ai-inference', () => ({
isUnexpected: jest.fn(() => false) 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) jest.unstable_mockModule('@actions/core', () => core)
// The module being tested should be imported dynamically. This ensures that the // 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') const { run } = await import('../src/main.js')
describe('main.ts', () => { describe('main.ts', () => {
// Reset all mocks before each test
beforeEach(() => { beforeEach(() => {
// Set the action's inputs as return values from core.getInput(). jest.clearAllMocks()
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 () => { 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() await run()
expect(core.setOutput).toHaveBeenNthCalledWith( expect(core.setOutput).toHaveBeenCalled()
1, verifyStandardResponse()
'response',
'Hello, user!'
)
}) })
it('Sets a failed status', async () => { it('Sets a failed status when no prompt is set', async () => {
// Clear the getInput mock and return an empty prompt // Clear the getInput mock and simulate no prompt or prompt-file input
core.getInput.mockClear().mockReturnValueOnce('') mockInputs({
prompt: '',
'prompt-file': ''
})
await run() await run()
// Verify that the action was marked as failed. // 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: inputs:
prompt: prompt:
description: The prompt for the model description: The prompt for the model
required: true required: false
default: ''
prompt-file:
description: Path to a file containing the prompt
required: false
default: '' default: ''
model: model:
description: The model to use description: The model to use
@@ -25,6 +29,10 @@ inputs:
description: The system prompt for the model description: The system prompt for the model
required: false required: false
default: 'You are a helpful assistant' default: 'You are a helpful assistant'
system-prompt-file:
description: Path to a file containing the system prompt
required: false
default: ''
max-tokens: max-tokens:
description: The maximum number of tokens to generate description: The maximum number of tokens to generate
required: false required: false
@@ -38,6 +46,8 @@ inputs:
outputs: outputs:
response: response:
description: The response from the model description: The response from the model
response-file:
description: The file path where the response is saved
runs: runs:
using: node20 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", "name": "typescript-action",
"description": "GitHub Actions TypeScript template", "description": "GitHub Actions TypeScript template",
"version": "0.0.0", "version": "1.0.0",
"author": "", "author": "",
"type": "module", "type": "module",
"private": true, "private": true,
@@ -32,7 +32,7 @@
"local-action": "npx @github/local-action . src/main.ts .env", "local-action": "npx @github/local-action . src/main.ts .env",
"package": "npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript", "package": "npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
"package:watch": "npm run package -- --watch", "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" "all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
}, },
"license": "MIT", "license": "MIT",
@@ -43,30 +43,30 @@
"@azure-rest/ai-inference": "latest", "@azure-rest/ai-inference": "latest",
"@azure/core-auth": "latest", "@azure/core-auth": "latest",
"@azure/core-sse": "latest", "@azure/core-sse": "latest",
"@eslint/compat": "^1.2.7", "@eslint/compat": "^1.2.9",
"@github/local-action": "^3.1.3", "@github/local-action": "^3.2.1",
"@jest/globals": "^29.7.0", "@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-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.1", "@rollup/plugin-typescript": "^12.1.2",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^20.17.28", "@types/node": "^22.15.21",
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.32.1",
"eslint": "^9.23.0", "eslint": "^9.27.0",
"eslint-config-prettier": "^10.0.2", "eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.3.1", "eslint-import-resolver-typescript": "^4.4.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0", "eslint-plugin-jest": "^28.11.0",
"eslint-plugin-prettier": "^5.2.5", "eslint-plugin-prettier": "^5.4.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"make-coverage-badge": "^1.2.0", "make-coverage-badge": "^1.2.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-eslint": "^16.3.0", "prettier-eslint": "^16.4.2",
"rollup": "^4.38.0", "rollup": "^4.41.1",
"ts-jest": "^29.3.0", "ts-jest": "^29.3.4",
"ts-jest-resolver": "^2.0.1", "ts-jest-resolver": "^2.0.1",
"typescript": "^5.8.2" "typescript": "^5.8.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "*" "@rollup/rollup-linux-x64-gnu": "*"

View File

@@ -1,6 +1,40 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference' import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
import { AzureKeyCredential } from '@azure/core-auth' 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. * The main function for the action.
@@ -9,12 +43,16 @@ import { AzureKeyCredential } from '@azure/core-auth'
*/ */
export async function run(): Promise<void> { export async function run(): Promise<void> {
try { try {
const prompt: string = core.getInput('prompt') // Load prompt content - required
if (prompt === undefined || prompt === '') { const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
throw new Error('prompt is not set')
} // 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 modelName: string = core.getInput('model')
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10) 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 // Set outputs for other workflow steps to use
core.setOutput('response', modelResponse || '') 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) { } catch (error) {
// Fail the workflow run if an error occurs // Fail the workflow run if an error occurs
if (error instanceof Error) { 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
}