21 Commits

Author SHA1 Message Date
Sean Goedecke
9693b137b6 Merge pull request #61 from actions/sgoedecke/prompt-file
Support .prompt.yml files
2025-07-21 15:14:53 +10:00
Sean Goedecke
d0b2f23c43 Merge branch 'main' into sgoedecke/prompt-file 2025-07-21 15:01:31 +10:00
Sean Goedecke
0df96479bc Merge pull request #60 from actions/sgoedecke/update-readme
Update readme to say MCP needs a PAT
2025-07-21 15:00:56 +10:00
Sean Goedecke
446f075e3b Merge branch 'main' into sgoedecke/update-readme 2025-07-21 15:00:09 +10:00
Sean Goedecke
ce58b26ac7 Merge pull request #59 from actions/sgoedecke-patch-1
Add GitHub Actions workflow for releasing new version
2025-07-21 14:57:40 +10:00
Sean Goedecke
1cf96b0212 Merge branch 'main' into sgoedecke/update-readme 2025-07-21 14:57:19 +10:00
Sean Goedecke
f79e4e11cb regenerate dist 2025-07-21 04:56:41 +00:00
Sean Goedecke
72102e50bf Update src/prompt.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 14:55:49 +10:00
Sean Goedecke
2bc30a525a regenerate dist 2025-07-21 04:35:30 +00:00
Sean Goedecke
8f64ac1284 Fixup types and tests 2025-07-21 04:31:06 +00:00
Sean Goedecke
1f89e942aa licensed cache 2025-07-21 04:25:01 +00:00
Sean Goedecke
77a7cbe11b Merge branch 'main' into sgoedecke-patch-1 2025-07-21 14:18:05 +10:00
Sean Goedecke
fcc8550115 lint 2025-07-21 04:01:22 +00:00
Sean Goedecke
29b5f08d0f Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:50:38 +10:00
Sean Goedecke
6f7bd88d1d lint 2025-07-21 03:49:57 +00:00
Yuzuki
8ae5306787 Merge pull request #62 from actions/dependabot/github_actions/super-linter/super-linter-8.0.0
Bump super-linter/super-linter from 7.4.0 to 8.0.0
2025-07-21 13:13:50 +10:00
dependabot[bot]
9445295106 Bump super-linter/super-linter from 7.4.0 to 8.0.0
Bumps [super-linter/super-linter](https://github.com/super-linter/super-linter) from 7.4.0 to 8.0.0.
- [Release notes](https://github.com/super-linter/super-linter/releases)
- [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md)
- [Commits](12150456a7...5119dcd801)

---
updated-dependencies:
- dependency-name: super-linter/super-linter
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 02:52:40 +00:00
Sean Goedecke
e385879671 Merge branch 'main' into sgoedecke/prompt-file 2025-07-21 00:21:07 +00:00
Sean Goedecke
1780121e3b Support .prompt.yml files 2025-07-21 00:11:26 +00:00
Sean Goedecke
b002da2928 Add GitHub Actions workflow for releasing new version 2025-07-21 09:41:47 +10:00
Sean Goedecke
ba509f9275 update readme 2025-07-16 07:51:13 +00:00
22 changed files with 4251 additions and 113 deletions

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@5119dcd8011e92182ce8219d9e9efc82f16fddb6
env:
DEFAULT_BRANCH: main
FILTER_REGEX_EXCLUDE: dist/**/*

View File

@@ -0,0 +1,22 @@
name: Release new action version
on:
release:
types: [released]
env:
TAG_NAME: ${{ github.event.release.tag_name }}
permissions:
contents: write
jobs:
update_tag:
name:
Update the major tag to include the ${{ github.event.release.tag_name }}
changes
runs-on: ubuntu-latest
steps:
- name: Update the ${{ env.TAG_NAME }} tag
uses: actions/publish-action@v0.2.2
with:
source-tag: ${{ env.TAG_NAME }}

View File

@@ -0,0 +1,32 @@
---
name: "@types/js-yaml"
version: 4.0.9
type: npm
summary: TypeScript definitions for js-yaml
homepage: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/js-yaml
license: mit
licenses:
- sources: LICENSE
text: |2
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
notices: []

View File

@@ -0,0 +1,265 @@
---
name: argparse
version: 2.0.1
type: npm
summary: CLI arguments parser. Native port of python's argparse.
homepage:
license: other
licenses:
- sources: LICENSE
text: |
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see http://www.opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the Internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the Internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
notices: []

View File

@@ -0,0 +1,32 @@
---
name: js-yaml
version: 4.1.0
type: npm
summary: YAML 1.2 parser and serializer
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
(The MIT License)
Copyright (C) 2011-2015 by Vitaly Puzrin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
notices: []

107
README.md
View File

@@ -36,17 +36,90 @@ jobs:
### Using a prompt file
You can also provide a prompt file instead of an inline prompt:
You can also provide a prompt file instead of an inline prompt. The action
supports both plain text files and structured `.prompt.yml` files:
```yaml
steps:
- name: Run AI Inference with Prompt File
- name: Run AI Inference with Text File
id: inference
uses: actions/ai-inference@v1
with:
prompt-file: './path/to/prompt.txt'
```
### Using GitHub prompt.yml files
For more advanced use cases, you can use structured `.prompt.yml` files that
support templating, custom models, and JSON schema responses:
```yaml
steps:
- name: Run AI Inference with Prompt YAML
id: inference
uses: actions/ai-inference@v1
with:
prompt-file: './.github/prompts/sample.prompt.yml'
input: |
var1: hello
var2: ${{ steps.some-step.outputs.output }}
var3: |
Lorem Ipsum
Hello World
```
#### Simple prompt.yml example
```yaml
messages:
- role: system
content: Be as concise as possible
- role: user
content: 'Compare {{a}} and {{b}}, please'
model: openai/gpt-4o
```
#### Prompt.yml with JSON schema support
```yaml
messages:
- role: system
content:
You are a helpful assistant that describes animals using JSON format
- role: user
content: |-
Describe a {{animal}}
Use JSON format as specified in the response schema
model: openai/gpt-4o
responseFormat: json_schema
jsonSchema: |-
{
"name": "describe_animal",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the animal"
},
"habitat": {
"type": "string",
"description": "The habitat the animal lives in"
}
},
"additionalProperties": false,
"required": [
"name",
"habitat"
]
}
}
```
Variables in prompt.yml files are templated using `{{variable}}` format and are
supplied via the `input` parameter in YAML format.
### Using a system prompt file
In addition to the regular prompt, you can provide a system prompt file instead
@@ -90,34 +163,36 @@ repository management, issue tracking, and pull request operations.
steps:
- name: AI Inference with GitHub Tools
id: inference
uses: actions/ai-inference@v1
uses: actions/ai-inference@v1.2
with:
prompt: 'List my open pull requests and create a summary'
enable-github-mcp: true
token: ${{ secrets.USER_PAT }}
```
When MCP is enabled, the AI model will have access to GitHub tools and can
perform actions like searching issues and PRs.
**Note:** MCP integration requires your workflow token to have appropriate
GitHub permissions for the operations the AI will perform.
**Note:** For now, MCP integration cannot be used with the built-in token. You
must pass a GitHub PAT into `token:` instead.
## 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 |
| `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 | `openai/gpt-4.1` |
| `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 |
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
| 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 (supports .txt and .prompt.yml formats). If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
| `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` |
| `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 | `openai/gpt-4o` |
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
| `max-tokens` | The max number of tokens to generate | 200 |
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
## Outputs

View File

@@ -0,0 +1,41 @@
messages:
- role: system
content:
You are a helpful assistant that describes animals using JSON format
- role: user
content: |-
Describe a {{animal}}
Use JSON format as specified in the response schema
model: openai/gpt-4o
responseFormat: json_schema
jsonSchema: |-
{
"name": "describe_animal",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the animal"
},
"habitat": {
"type": "string",
"description": "The habitat the animal lives in"
},
"characteristics": {
"type": "array",
"items": {
"type": "string"
},
"description": "Key characteristics of the animal"
}
},
"additionalProperties": false,
"required": [
"name",
"habitat",
"characteristics"
]
}
}

View File

@@ -0,0 +1,6 @@
messages:
- role: system
content: Be as concise as possible
- role: user
content: 'Compare {{a}} and {{b}}, please'
model: openai/gpt-4o

View File

@@ -0,0 +1,163 @@
import { describe, it, expect } from '@jest/globals'
import {
buildMessages,
buildResponseFormat,
buildInferenceRequest
} from '../src/helpers'
import { PromptConfig } from '../src/prompt'
describe('helpers.ts - inference request building', () => {
describe('buildMessages', () => {
it('should build messages from prompt config', () => {
const promptConfig: PromptConfig = {
messages: [
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
]
}
const result = buildMessages(promptConfig)
expect(result).toEqual([
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
])
})
it('should build messages from legacy format', () => {
const result = buildMessages(undefined, 'System prompt', 'User prompt')
expect(result).toEqual([
{ role: 'system', content: 'System prompt' },
{ role: 'user', content: 'User prompt' }
])
})
it('should use default system prompt when none provided', () => {
const result = buildMessages(undefined, undefined, 'User prompt')
expect(result).toEqual([
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: 'User prompt' }
])
})
})
describe('buildResponseFormat', () => {
it('should build JSON schema response format', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'json_schema',
jsonSchema: JSON.stringify({
name: 'test_schema',
schema: { type: 'object' }
})
}
const result = buildResponseFormat(promptConfig)
expect(result).toEqual({
type: 'json_schema',
json_schema: {
name: 'test_schema',
schema: { type: 'object' }
}
})
})
it('should return undefined for text format', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'text'
}
const result = buildResponseFormat(promptConfig)
expect(result).toBeUndefined()
})
it('should return undefined when no response format specified', () => {
const promptConfig: PromptConfig = {
messages: []
}
const result = buildResponseFormat(promptConfig)
expect(result).toBeUndefined()
})
it('should throw error for invalid JSON schema', () => {
const promptConfig: PromptConfig = {
messages: [],
responseFormat: 'json_schema',
jsonSchema: 'invalid json'
}
expect(() => buildResponseFormat(promptConfig)).toThrow(
'Invalid JSON schema'
)
})
})
describe('buildInferenceRequest', () => {
it('should build complete inference request from prompt config', () => {
const promptConfig: PromptConfig = {
messages: [
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
],
responseFormat: 'json_schema',
jsonSchema: JSON.stringify({
name: 'test_schema',
schema: { type: 'object' }
})
}
const result = buildInferenceRequest(
promptConfig,
undefined,
undefined,
'gpt-4',
100,
'https://api.test.com',
'test-token'
)
expect(result).toEqual({
messages: [
{ role: 'system', content: 'System message' },
{ role: 'user', content: 'User message' }
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'test-token',
responseFormat: {
type: 'json_schema',
json_schema: {
name: 'test_schema',
schema: { type: 'object' }
}
}
})
})
it('should build inference request from legacy format', () => {
const result = buildInferenceRequest(
undefined,
'System prompt',
'User prompt',
'gpt-4',
100,
'https://api.test.com',
'test-token'
)
expect(result).toEqual({
messages: [
{ role: 'system', content: 'System prompt' },
{ role: 'user', content: 'User prompt' }
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'test-token',
responseFormat: undefined
})
})
})
})

View File

@@ -33,8 +33,10 @@ const { simpleInference, mcpInference } = await import('../src/inference.js')
describe('inference.ts', () => {
const mockRequest = {
systemPrompt: 'You are a test assistant',
prompt: 'Hello, AI!',
messages: [
{ role: 'system', content: 'You are a test assistant' },
{ role: 'user', content: 'Hello, AI!' }
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',

View File

@@ -0,0 +1,182 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals'
import * as core from '../__fixtures__/core.js'
// Create fs mocks
const mockExistsSync = jest.fn()
const mockReadFileSync = jest.fn()
const mockWriteFileSync = jest.fn()
// Create inference mocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockSimpleInference = jest.fn() as jest.MockedFunction<any>
const mockMcpInference = jest.fn()
// Create MCP mocks
const mockConnectToGitHubMCP = jest.fn()
// Mock fs module
jest.unstable_mockModule('fs', () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync
}))
// Mock the inference functions
jest.unstable_mockModule('../src/inference.js', () => ({
simpleInference: mockSimpleInference,
mcpInference: mockMcpInference
}))
// Mock the MCP connection
jest.unstable_mockModule('../src/mcp.js', () => ({
connectToGitHubMCP: mockConnectToGitHubMCP
}))
jest.unstable_mockModule('@actions/core', () => core)
// The module being tested should be imported dynamically. This ensures that the
// mocks are used in place of any actual dependencies.
const { run } = await import('../src/main.js')
describe('main.ts - prompt.yml integration', () => {
beforeEach(() => {
jest.clearAllMocks()
// Mock environment variables
process.env['GITHUB_TOKEN'] = 'test-token'
// Mock core.getInput to return appropriate values
core.getInput.mockImplementation((name: string) => {
switch (name) {
case 'model':
return 'openai/gpt-4o'
case 'max-tokens':
return '200'
case 'endpoint':
return 'https://models.github.ai/inference'
case 'enable-github-mcp':
return 'false'
default:
return ''
}
})
// Mock core.getBooleanInput
const mockGetBooleanInput = core.getBooleanInput as jest.Mock
mockGetBooleanInput.mockReturnValue(false)
// Mock fs.readFileSync for prompt file
mockReadFileSync.mockReturnValue(`
messages:
- role: system
content: Be as concise as possible
- role: user
content: 'Compare {{a}} and {{b}}, please'
model: openai/gpt-4o
`)
// Mock fs.writeFileSync
mockWriteFileSync.mockImplementation(() => {})
// Mock simpleInference
mockSimpleInference.mockResolvedValue('Mocked AI response')
})
it('should handle prompt YAML files with template variables', async () => {
mockExistsSync.mockReturnValue(true)
core.getInput.mockImplementation((name: string) => {
switch (name) {
case 'prompt-file':
return 'test.prompt.yml'
case 'input':
return 'a: cats\nb: dogs'
case 'model':
return 'openai/gpt-4o'
case 'max-tokens':
return '200'
case 'endpoint':
return 'https://models.github.ai/inference'
case 'enable-github-mcp':
return 'false'
default:
return ''
}
})
await run()
// Verify simpleInference was called with the correct message structure
expect(mockSimpleInference).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
{
role: 'system',
content: 'Be as concise as possible'
},
{
role: 'user',
content: 'Compare cats and dogs, please'
}
],
modelName: 'openai/gpt-4o',
maxTokens: 200,
endpoint: 'https://models.github.ai/inference',
token: 'test-token'
})
)
// Verify outputs were set
expect(core.setOutput).toHaveBeenCalledWith(
'response',
'Mocked AI response'
)
expect(core.setOutput).toHaveBeenCalledWith(
'response-file',
expect.any(String)
)
})
it('should fall back to legacy format when not using prompt YAML', async () => {
mockExistsSync.mockReturnValue(false)
core.getInput.mockImplementation((name: string) => {
switch (name) {
case 'prompt':
return 'Hello, world!'
case 'system-prompt':
return 'You are helpful'
case 'model':
return 'openai/gpt-4o'
case 'max-tokens':
return '200'
case 'endpoint':
return 'https://models.github.ai/inference'
case 'enable-github-mcp':
return 'false'
default:
return ''
}
})
await run()
// Verify simpleInference was called with converted message format
expect(mockSimpleInference).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
{
role: 'system',
content: 'You are helpful'
},
{
role: 'user',
content: 'Hello, world!'
}
],
modelName: 'openai/gpt-4o',
maxTokens: 200,
endpoint: 'https://models.github.ai/inference',
token: 'test-token'
})
)
})
})

View File

@@ -161,12 +161,15 @@ describe('main.ts', () => {
await run()
expect(mockSimpleInference).toHaveBeenCalledWith({
systemPrompt: 'You are a test assistant.',
prompt: 'Hello, AI!',
messages: [
{ role: 'system', content: 'You are a test assistant.' },
{ role: 'user', content: 'Hello, AI!' }
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'fake-token'
token: 'fake-token',
responseFormat: undefined
})
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
expect(mockMcpInference).not.toHaveBeenCalled()
@@ -193,8 +196,10 @@ describe('main.ts', () => {
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
expect(mockMcpInference).toHaveBeenCalledWith(
expect.objectContaining({
systemPrompt: 'You are a test assistant.',
prompt: 'Hello, AI!',
messages: [
{ role: 'system', content: 'You are a test assistant.' },
{ role: 'user', content: 'Hello, AI!' }
],
token: 'fake-token'
}),
mockMcpClient
@@ -243,12 +248,15 @@ describe('main.ts', () => {
await run()
expect(mockSimpleInference).toHaveBeenCalledWith({
systemPrompt: systemPromptContent,
prompt: promptContent,
messages: [
{ role: 'system', content: systemPromptContent },
{ role: 'user', content: promptContent }
],
modelName: 'gpt-4',
maxTokens: 100,
endpoint: 'https://api.test.com',
token: 'fake-token'
token: 'fake-token',
responseFormat: undefined
})
verifyStandardResponse()
})

133
__tests__/prompt.test.ts Normal file
View File

@@ -0,0 +1,133 @@
import { describe, it, expect } from '@jest/globals'
import * as path from 'path'
import { fileURLToPath } from 'url'
import {
parseTemplateVariables,
replaceTemplateVariables,
loadPromptFile,
isPromptYamlFile
} from '../src/prompt'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
describe('prompt.ts', () => {
describe('parseTemplateVariables', () => {
it('should parse simple YAML variables', () => {
const input = `
a: hello
b: world
`
const result = parseTemplateVariables(input)
expect(result).toEqual({ a: 'hello', b: 'world' })
})
it('should parse multiline variables', () => {
const input = `
var1: hello
var2: |
This is a
multiline string
`
const result = parseTemplateVariables(input)
expect(result.var1).toBe('hello')
expect(result.var2).toContain('This is a')
expect(result.var2).toContain('multiline string')
})
it('should return empty object for empty input', () => {
const result = parseTemplateVariables('')
expect(result).toEqual({})
})
it('should throw error for invalid YAML', () => {
const input = 'invalid: yaml: content:'
expect(() => parseTemplateVariables(input)).toThrow()
})
})
describe('replaceTemplateVariables', () => {
it('should replace simple variables', () => {
const text = 'Hello {{name}}, welcome to {{place}}!'
const variables = { name: 'John', place: 'GitHub' }
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('Hello John, welcome to GitHub!')
})
it('should leave unreplaced variables as is', () => {
const text = 'Hello {{name}}, welcome to {{unknown}}!'
const variables = { name: 'John' }
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('Hello John, welcome to {{unknown}}!')
})
it('should handle no variables', () => {
const text = 'No variables here'
const variables = {}
const result = replaceTemplateVariables(text, variables)
expect(result).toBe('No variables here')
})
})
describe('isPromptYamlFile', () => {
it('should detect .prompt.yml files', () => {
expect(isPromptYamlFile('test.prompt.yml')).toBe(true)
expect(isPromptYamlFile('path/to/test.prompt.yml')).toBe(true)
})
it('should detect .prompt.yaml files', () => {
expect(isPromptYamlFile('test.prompt.yaml')).toBe(true)
expect(isPromptYamlFile('path/to/test.prompt.yaml')).toBe(true)
})
it('should reject other file types', () => {
expect(isPromptYamlFile('test.txt')).toBe(false)
expect(isPromptYamlFile('test.yml')).toBe(false)
expect(isPromptYamlFile('test.yaml')).toBe(false)
expect(isPromptYamlFile('test.prompt')).toBe(false)
})
})
describe('loadPromptFile', () => {
it('should load simple prompt file', () => {
const filePath = path.join(
__dirname,
'../__fixtures__/prompts/simple.prompt.yml'
)
const variables = { a: 'cats', b: 'dogs' }
const result = loadPromptFile(filePath, variables)
expect(result.messages).toHaveLength(2)
expect(result.messages[0]).toEqual({
role: 'system',
content: 'Be as concise as possible'
})
expect(result.messages[1]).toEqual({
role: 'user',
content: 'Compare cats and dogs, please'
})
expect(result.model).toBe('openai/gpt-4o')
})
it('should load JSON schema prompt file', () => {
const filePath = path.join(
__dirname,
'../__fixtures__/prompts/json-schema.prompt.yml'
)
const variables = { animal: 'dog' }
const result = loadPromptFile(filePath, variables)
expect(result.messages).toHaveLength(2)
expect(result.messages[1].content).toContain('Describe a dog')
expect(result.responseFormat).toBe('json_schema')
expect(result.jsonSchema).toBeDefined()
expect(result.jsonSchema).toContain('describe_animal')
})
it('should throw error for non-existent file', () => {
expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow(
'Prompt file not found'
)
})
})
})

View File

@@ -14,7 +14,13 @@ inputs:
required: false
default: ''
prompt-file:
description: Path to a file containing the prompt
description:
Path to a file containing the prompt (supports .txt and .prompt.yml
formats)
required: false
default: ''
input:
description: Template variables in YAML format for .prompt.yml files
required: false
default: ''
model:

3028
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

10
package-lock.json generated
View File

@@ -12,6 +12,8 @@
"@actions/core": "^1.11.1",
"@modelcontextprotocol/sdk": "^1.15.1",
"@rollup/plugin-json": "^6.1.0",
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0",
"pkce-challenge": "^5.0.0"
},
"devDependencies": {
@@ -4407,6 +4409,12 @@
"pretty-format": "^29.0.0"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -5173,7 +5181,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-buffer-byte-length": {
@@ -10254,7 +10261,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"

View File

@@ -40,6 +40,8 @@
"@actions/core": "^1.11.1",
"@modelcontextprotocol/sdk": "^1.15.1",
"@rollup/plugin-json": "^6.1.0",
"@types/js-yaml": "^4.0.9",
"js-yaml": "^4.1.0",
"pkce-challenge": "^5.0.0"
},
"devDependencies": {

View File

@@ -1,6 +1,8 @@
import * as core from '@actions/core'
import { GetChatCompletionsDefaultResponse } from '@azure-rest/ai-inference'
import * as fs from 'fs'
import { PromptConfig } from './prompt.js'
import { InferenceRequest } from './inference.js'
/**
* Helper function to load content from a file or use fallback input
@@ -64,3 +66,79 @@ export function handleUnexpectedResponse(
: JSON.stringify(response.body))
)
}
/**
* Build messages array from either prompt config or legacy format
*/
export function buildMessages(
promptConfig?: PromptConfig,
systemPrompt?: string,
prompt?: string
): Array<{ role: string; content: string }> {
if (promptConfig?.messages && promptConfig.messages.length > 0) {
// Use new message format
return promptConfig.messages.map((msg) => ({
role: msg.role,
content: msg.content
}))
} else {
// Use legacy format
return [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant'
},
{ role: 'user', content: prompt || '' }
]
}
}
/**
* Build response format object for API from prompt config
*/
export function buildResponseFormat(
promptConfig?: PromptConfig
): { type: 'json_schema'; json_schema: unknown } | undefined {
if (
promptConfig?.responseFormat === 'json_schema' &&
promptConfig.jsonSchema
) {
try {
const schema = JSON.parse(promptConfig.jsonSchema)
return {
type: 'json_schema',
json_schema: schema
}
} catch (error) {
throw new Error(
`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
return undefined
}
/**
* Build complete InferenceRequest from prompt config and inputs
*/
export function buildInferenceRequest(
promptConfig: PromptConfig | undefined,
systemPrompt: string | undefined,
prompt: string | undefined,
modelName: string,
maxTokens: number,
endpoint: string,
token: string
): InferenceRequest {
const messages = buildMessages(promptConfig, systemPrompt, prompt)
const responseFormat = buildResponseFormat(promptConfig)
return {
messages,
modelName,
maxTokens,
endpoint,
token,
responseFormat
}
}

View File

@@ -1,16 +1,30 @@
import * as core from '@actions/core'
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
import { AzureKeyCredential } from '@azure/core-auth'
import { GitHubMCPClient, executeToolCalls } from './mcp.js'
import { GitHubMCPClient, executeToolCalls, MCPTool, ToolCall } from './mcp.js'
import { handleUnexpectedResponse } from './helpers.js'
interface ChatMessage {
role: string
content: string | null
tool_calls?: ToolCall[]
}
interface ChatCompletionsRequestBody {
messages: ChatMessage[]
max_tokens: number
model: string
response_format?: { type: 'json_schema'; json_schema: unknown }
tools?: MCPTool[]
}
export interface InferenceRequest {
systemPrompt: string
prompt: string
messages: Array<{ role: string; content: string }>
modelName: string
maxTokens: number
endpoint: string
token: string
responseFormat?: { type: 'json_schema'; json_schema: unknown } // Processed response format for the API
}
export interface InferenceResponse {
@@ -41,18 +55,17 @@ export async function simpleInference(
}
)
const requestBody = {
messages: [
{
role: 'system',
content: request.systemPrompt
},
{ role: 'user', content: request.prompt }
],
const requestBody: ChatCompletionsRequestBody = {
messages: request.messages,
max_tokens: request.maxTokens,
model: request.modelName
}
// Add response format if specified
if (request.responseFormat) {
requestBody.response_format = request.responseFormat
}
const response = await client.path('/chat/completions').post({
body: requestBody
})
@@ -84,14 +97,8 @@ export async function mcpInference(
}
)
// Start with the initial conversation
const messages = [
{
role: 'system',
content: request.systemPrompt
},
{ role: 'user', content: request.prompt }
]
// Start with the pre-processed messages
const messages: ChatMessage[] = [...request.messages]
let iterationCount = 0
const maxIterations = 5 // Prevent infinite loops
@@ -100,13 +107,18 @@ export async function mcpInference(
iterationCount++
core.info(`MCP inference iteration ${iterationCount}`)
const requestBody = {
const requestBody: ChatCompletionsRequestBody = {
messages: messages,
max_tokens: request.maxTokens,
model: request.modelName,
tools: githubMcpClient.tools
}
// Add response format if specified (only on first iteration to avoid conflicts)
if (iterationCount === 1 && request.responseFormat) {
requestBody.response_format = request.responseFormat
}
const response = await client.path('/chat/completions').post({
body: requestBody
})

View File

@@ -3,8 +3,14 @@ import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import { connectToGitHubMCP } from './mcp.js'
import { simpleInference, mcpInference, InferenceRequest } from './inference.js'
import { loadContentFromFileOrInput } from './helpers.js'
import { simpleInference, mcpInference } from './inference.js'
import { loadContentFromFileOrInput, buildInferenceRequest } from './helpers.js'
import {
loadPromptFile,
parseTemplateVariables,
isPromptYamlFile,
PromptConfig
} from './prompt.js'
const RESPONSE_FILE = 'modelResponse.txt'
@@ -15,16 +21,37 @@ const RESPONSE_FILE = 'modelResponse.txt'
*/
export async function run(): Promise<void> {
try {
const prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
const promptFilePath = core.getInput('prompt-file')
const inputVariables = core.getInput('input')
const systemPrompt = loadContentFromFileOrInput(
'system-prompt-file',
'system-prompt',
'You are a helpful assistant'
)
let promptConfig: PromptConfig | undefined = undefined
let systemPrompt: string | undefined = undefined
let prompt: string | undefined = undefined
const modelName: string = core.getInput('model')
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
// Check if we're using a prompt YAML file
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
core.info('Using prompt YAML file format')
// Parse template variables
const templateVariables = parseTemplateVariables(inputVariables)
// Load and process prompt file
promptConfig = loadPromptFile(promptFilePath, templateVariables)
} else {
// Use legacy format
core.info('Using legacy prompt format')
prompt = loadContentFromFileOrInput('prompt-file', 'prompt')
systemPrompt = loadContentFromFileOrInput(
'system-prompt-file',
'system-prompt',
'You are a helpful assistant'
)
}
// Get common parameters
const modelName = promptConfig?.model || core.getInput('model')
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
if (token === undefined) {
@@ -32,21 +59,24 @@ export async function run(): Promise<void> {
}
const endpoint = core.getInput('endpoint')
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
const inferenceRequest: InferenceRequest = {
// Build the inference request with pre-processed messages and response format
const inferenceRequest = buildInferenceRequest(
promptConfig,
systemPrompt,
prompt,
modelName,
maxTokens,
endpoint,
token
}
)
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
let modelResponse: string | null = null
if (enableMcp) {
const mcpClient = await connectToGitHubMCP(token)
const mcpClient = await connectToGitHubMCP(inferenceRequest.token)
if (mcpClient) {
modelResponse = await mcpInference(inferenceRequest, mcpClient)

111
src/prompt.ts Normal file
View File

@@ -0,0 +1,111 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
export interface PromptMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
export interface PromptConfig {
messages: PromptMessage[]
model?: string
responseFormat?: 'text' | 'json_schema'
jsonSchema?: string
}
export interface TemplateVariables {
[key: string]: string
}
/**
* Parse template variables from YAML input string
*/
export function parseTemplateVariables(input: string): TemplateVariables {
if (!input.trim()) {
return {}
}
try {
const parsed = yaml.load(input) as TemplateVariables
if (typeof parsed !== 'object' || parsed === null) {
throw new Error('Template variables must be a YAML object')
}
return parsed
} catch (error) {
throw new Error(
`Failed to parse template variables: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
/**
* Replace template variables in text using {{variable}} syntax
*/
export function replaceTemplateVariables(
text: string,
variables: TemplateVariables
): string {
return text.replace(/\{\{([\w.-]+)\}\}/g, (match, variableName) => {
if (variableName in variables) {
return variables[variableName]
}
core.warning(
`Template variable '${variableName}' not found in input variables`
)
return match // Return the original placeholder if variable not found
})
}
/**
* Load and parse a prompt YAML file with template variable substitution
*/
export function loadPromptFile(
filePath: string,
templateVariables: TemplateVariables = {}
): PromptConfig {
if (!fs.existsSync(filePath)) {
throw new Error(`Prompt file not found: ${filePath}`)
}
const fileContent = fs.readFileSync(filePath, 'utf-8')
// Apply template variable substitution
const processedContent = replaceTemplateVariables(
fileContent,
templateVariables
)
try {
const config = yaml.load(processedContent) as PromptConfig
if (!config.messages || !Array.isArray(config.messages)) {
throw new Error('Prompt file must contain a "messages" array')
}
// Validate messages
for (const message of config.messages) {
if (!message.role || !message.content) {
throw new Error(
'Each message must have "role" and "content" properties'
)
}
if (!['system', 'user', 'assistant'].includes(message.role)) {
throw new Error(`Invalid message role: ${message.role}`)
}
}
return config
} catch (error) {
throw new Error(
`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
/**
* Check if a file is a prompt YAML file based on extension
*/
export function isPromptYamlFile(filePath: string): boolean {
return filePath.endsWith('.prompt.yml') || filePath.endsWith('.prompt.yaml')
}