Skip to content

Commit 3c6ec33

Browse files
authored
Merge pull request #85 from actions/sgoedecke/file-inputs
Allow templating variables from files
2 parents c37f296 + ea4e7d8 commit 3c6ec33

File tree

8 files changed

+174
-14
lines changed

8 files changed

+174
-14
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ steps:
6565
var3: |
6666
Lorem Ipsum
6767
Hello World
68+
file_input: |
69+
var4: ./path/to/long-text.txt
70+
var5: ./path/to/config.json
6871
```
6972

7073
#### Simple prompt.yml example
@@ -116,7 +119,9 @@ jsonSchema: |-
116119
```
117120

118121
Variables in prompt.yml files are templated using `{{variable}}` format and are
119-
supplied via the `input` parameter in YAML format.
122+
supplied via the `input` parameter in YAML format. Additionally, you can
123+
provide file-based variables via `file_input`, where each key maps to a file
124+
path.
120125

121126
### Using a system prompt file
122127

@@ -197,6 +202,7 @@ the action:
197202
| `prompt` | The prompt to send to the model | N/A |
198203
| `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 | `""` |
199204
| `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` |
205+
| `file_input` | Template variables in YAML where values are file paths. The file contents are read and used for templating | `""` |
200206
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
201207
| `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 | `""` |
202208
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |

__tests__/main-prompt-integration.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,49 @@ model: openai/gpt-4o
130130
expect(core.setOutput).toHaveBeenCalledWith('response-file', expect.any(String))
131131
})
132132

133+
it('supports file_input variables to load file contents', async () => {
134+
mockExistsSync.mockReturnValue(true)
135+
136+
// First call: reading the prompt file. Second call: reading file_input referenced file contents.
137+
const externalFilePath = 'vars.txt'
138+
mockReadFileSync.mockImplementation((path: string) => {
139+
if (path === 'test.prompt.yml') {
140+
return `messages:\n - role: user\n content: 'Here is the data: {{blob}}'\nmodel: openai/gpt-4o\n`
141+
}
142+
if (path === externalFilePath) {
143+
return 'FILE_CONTENTS'
144+
}
145+
return ''
146+
})
147+
148+
core.getInput.mockImplementation((name: string) => {
149+
switch (name) {
150+
case 'prompt-file':
151+
return 'test.prompt.yml'
152+
case 'file_input':
153+
return `blob: ${externalFilePath}`
154+
case 'model':
155+
return 'openai/gpt-4o'
156+
case 'max-tokens':
157+
return '200'
158+
case 'endpoint':
159+
return 'https://models.github.ai/inference'
160+
case 'enable-github-mcp':
161+
return 'false'
162+
default:
163+
return ''
164+
}
165+
})
166+
167+
await run()
168+
169+
expect(mockSimpleInference).toHaveBeenCalledWith(
170+
expect.objectContaining({
171+
messages: [{role: 'user', content: 'Here is the data: FILE_CONTENTS'}],
172+
}),
173+
)
174+
})
175+
133176
it('should fall back to legacy format when not using prompt YAML', async () => {
134177
mockExistsSync.mockReturnValue(false)
135178
core.getInput.mockImplementation((name: string) => {

__tests__/prompt.test.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import {describe, it, expect} from 'vitest'
22
import * as path from 'path'
33
import {fileURLToPath} from 'url'
4-
import {parseTemplateVariables, replaceTemplateVariables, loadPromptFile, isPromptYamlFile} from '../src/prompt'
4+
import {
5+
parseTemplateVariables,
6+
replaceTemplateVariables,
7+
loadPromptFile,
8+
isPromptYamlFile,
9+
parseFileTemplateVariables,
10+
} from '../src/prompt'
511

612
const __filename = fileURLToPath(import.meta.url)
713
const __dirname = path.dirname(__filename)
@@ -10,19 +16,19 @@ describe('prompt.ts', () => {
1016
describe('parseTemplateVariables', () => {
1117
it('should parse simple YAML variables', () => {
1218
const input = `
13-
a: hello
14-
b: world
19+
a: hello
20+
b: world
1521
`
1622
const result = parseTemplateVariables(input)
1723
expect(result).toEqual({a: 'hello', b: 'world'})
1824
})
1925

2026
it('should parse multiline variables', () => {
2127
const input = `
22-
var1: hello
23-
var2: |
24-
This is a
25-
multiline string
28+
var1: hello
29+
var2: |
30+
This is a
31+
multiline string
2632
`
2733
const result = parseTemplateVariables(input)
2834
expect(result.var1).toBe('hello')
@@ -117,4 +123,17 @@ var2: |
117123
expect(() => loadPromptFile('non-existent.prompt.yml')).toThrow('Prompt file not found')
118124
})
119125
})
126+
127+
describe('parseFileTemplateVariables', () => {
128+
it('reads file contents for variables', () => {
129+
const configPath = path.join(__dirname, '../__fixtures__/prompts/json-schema.prompt.yml')
130+
const data = parseFileTemplateVariables(`sample: ${configPath}`)
131+
expect(data.sample).toContain('messages:')
132+
expect(data.sample).toContain('responseFormat:')
133+
})
134+
135+
it('errors on missing files', () => {
136+
expect(() => parseFileTemplateVariables('x: ./does-not-exist.txt')).toThrow('was not found')
137+
})
138+
})
120139
})

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ inputs:
2222
description: Template variables in YAML format for .prompt.yml files
2323
required: false
2424
default: ''
25+
file_input:
26+
description: Template variables in YAML format mapping variable names to file paths. The file contents will be used for templating.
27+
required: false
28+
default: ''
2529
model:
2630
description: The model to use
2731
required: false

dist/index.js

Lines changed: 40 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import * as path from 'path'
55
import {connectToGitHubMCP} from './mcp.js'
66
import {simpleInference, mcpInference} from './inference.js'
77
import {loadContentFromFileOrInput, buildInferenceRequest} from './helpers.js'
8-
import {loadPromptFile, parseTemplateVariables, isPromptYamlFile, PromptConfig} from './prompt.js'
8+
import {
9+
loadPromptFile,
10+
parseTemplateVariables,
11+
isPromptYamlFile,
12+
PromptConfig,
13+
parseFileTemplateVariables,
14+
} from './prompt.js'
915

1016
const RESPONSE_FILE = 'modelResponse.txt'
1117

@@ -18,6 +24,7 @@ export async function run(): Promise<void> {
1824
try {
1925
const promptFilePath = core.getInput('prompt-file')
2026
const inputVariables = core.getInput('input')
27+
const fileInputVariables = core.getInput('file_input')
2128

2229
let promptConfig: PromptConfig | undefined = undefined
2330
let systemPrompt: string | undefined = undefined
@@ -27,8 +34,10 @@ export async function run(): Promise<void> {
2734
if (promptFilePath && isPromptYamlFile(promptFilePath)) {
2835
core.info('Using prompt YAML file format')
2936

30-
// Parse template variables
31-
const templateVariables = parseTemplateVariables(inputVariables)
37+
// Parse template variables from both string inputs and file-based inputs
38+
const stringVars = parseTemplateVariables(inputVariables)
39+
const fileVars = parseFileTemplateVariables(fileInputVariables)
40+
const templateVariables = {...stringVars, ...fileVars}
3241

3342
// Load and process prompt file
3443
promptConfig = loadPromptFile(promptFilePath, templateVariables)

src/prompt.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,47 @@ export function parseTemplateVariables(input: string): TemplateVariables {
3737
}
3838
}
3939

40+
/**
41+
* Parse file-based template variables from YAML input string. The YAML should map
42+
* variable names to file paths. File contents are read and returned as variables.
43+
*/
44+
export function parseFileTemplateVariables(fileInput: string): TemplateVariables {
45+
if (!fileInput.trim()) {
46+
return {}
47+
}
48+
49+
try {
50+
const parsed = yaml.load(fileInput) as Record<string, unknown>
51+
if (typeof parsed !== 'object' || parsed === null) {
52+
throw new Error('File template variables must be a YAML object')
53+
}
54+
55+
const result: TemplateVariables = {}
56+
for (const [key, value] of Object.entries(parsed)) {
57+
if (typeof value !== 'string') {
58+
throw new Error(`File template variable '${key}' must be a string file path`)
59+
}
60+
const filePath = value
61+
if (!fs.existsSync(filePath)) {
62+
throw new Error(`File for template variable '${key}' was not found: ${filePath}`)
63+
}
64+
try {
65+
result[key] = fs.readFileSync(filePath, 'utf-8')
66+
} catch (err) {
67+
throw new Error(
68+
`Failed to read file for template variable '${key}' at path '${filePath}': ${err instanceof Error ? err.message : 'Unknown error'}`,
69+
)
70+
}
71+
}
72+
73+
return result
74+
} catch (error) {
75+
throw new Error(
76+
`Failed to parse file template variables: ${error instanceof Error ? error.message : 'Unknown error'}`,
77+
)
78+
}
79+
}
80+
4081
/**
4182
* Replace template variables in text using {{variable}} syntax
4283
*/

0 commit comments

Comments
 (0)