Skip to content

Commit f6b34dc

Browse files
mldangeloclaude
andauthored
docs: Document environment variable passthrough (#705)
Co-authored-by: Claude <[email protected]>
1 parent aca8561 commit f6b34dc

File tree

6 files changed

+225
-16
lines changed

6 files changed

+225
-16
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ The following API key parameters are supported:
5959
| `mistral-api-key` | The API key for Mistral. Used to authenticate requests to the Mistral API. |
6060
| `groq-api-key` | The API key for Groq. Used to authenticate requests to the Groq API. |
6161

62+
### Environment Variables
63+
64+
All workflow environment variables are passed through to promptfoo. You can set API keys at the job/workflow level instead of as action inputs:
65+
66+
```yaml
67+
env:
68+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
69+
70+
steps:
71+
- uses: promptfoo/promptfoo-action@v1
72+
with:
73+
github-token: ${{ secrets.GITHUB_TOKEN }}
74+
config: 'promptfooconfig.yaml'
75+
```
76+
77+
Action inputs take precedence over environment variables. See action.yml for the complete mapping of input parameters to environment variables.
78+
6279
## Usage Examples
6380
6481
### Pull Request Evaluation

__tests__/main.test.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,3 +1059,190 @@ describe('disable-comment feature', () => {
10591059
expect(readmeContent).toContain('Disable posting comments to the PR');
10601060
});
10611061
});
1062+
1063+
describe('API key environment variable fallback', () => {
1064+
beforeEach(() => {
1065+
// Clear all environment variables before each test
1066+
delete process.env.OPENAI_API_KEY;
1067+
delete process.env.ANTHROPIC_API_KEY;
1068+
delete process.env.AZURE_OPENAI_API_KEY;
1069+
delete process.env.HF_API_TOKEN;
1070+
});
1071+
1072+
test('should use env var when action input not provided', async () => {
1073+
process.env.OPENAI_API_KEY = 'env-openai-key';
1074+
process.env.ANTHROPIC_API_KEY = 'env-anthropic-key';
1075+
1076+
mockCore.getInput.mockImplementation((name: string) => {
1077+
const inputs: Record<string, string> = {
1078+
'github-token': 'mock-github-token',
1079+
config: 'promptfooconfig.yaml',
1080+
prompts: 'prompts/*.txt',
1081+
'openai-api-key': '', // Not provided
1082+
'anthropic-api-key': '', // Not provided
1083+
};
1084+
return inputs[name] || '';
1085+
});
1086+
1087+
await run();
1088+
1089+
const envPassedToExec = mockExec.exec.mock.calls[0][2] as {
1090+
env: Record<string, string>;
1091+
};
1092+
expect(envPassedToExec.env.OPENAI_API_KEY).toBe('env-openai-key');
1093+
expect(envPassedToExec.env.ANTHROPIC_API_KEY).toBe('env-anthropic-key');
1094+
});
1095+
1096+
test('should prefer action input over env var', async () => {
1097+
process.env.OPENAI_API_KEY = 'env-openai-key';
1098+
process.env.ANTHROPIC_API_KEY = 'env-anthropic-key';
1099+
1100+
mockCore.getInput.mockImplementation((name: string) => {
1101+
const inputs: Record<string, string> = {
1102+
'github-token': 'mock-github-token',
1103+
config: 'promptfooconfig.yaml',
1104+
prompts: 'prompts/*.txt',
1105+
'openai-api-key': 'input-openai-key', // Provided via input
1106+
'anthropic-api-key': 'input-anthropic-key', // Provided via input
1107+
};
1108+
return inputs[name] || '';
1109+
});
1110+
1111+
await run();
1112+
1113+
const envPassedToExec = mockExec.exec.mock.calls[0][2] as {
1114+
env: Record<string, string>;
1115+
};
1116+
expect(envPassedToExec.env.OPENAI_API_KEY).toBe('input-openai-key');
1117+
expect(envPassedToExec.env.ANTHROPIC_API_KEY).toBe('input-anthropic-key');
1118+
});
1119+
1120+
test('should work for all API key providers', async () => {
1121+
process.env.OPENAI_API_KEY = 'openai-env';
1122+
process.env.AZURE_OPENAI_API_KEY = 'azure-env';
1123+
process.env.ANTHROPIC_API_KEY = 'anthropic-env';
1124+
process.env.HF_API_TOKEN = 'hf-env';
1125+
process.env.AWS_ACCESS_KEY_ID = 'aws-key-id-env';
1126+
process.env.AWS_SECRET_ACCESS_KEY = 'aws-secret-env';
1127+
process.env.REPLICATE_API_KEY = 'replicate-env';
1128+
process.env.PALM_API_KEY = 'palm-env';
1129+
process.env.VERTEX_API_KEY = 'vertex-env';
1130+
process.env.COHERE_API_KEY = 'cohere-env';
1131+
process.env.MISTRAL_API_KEY = 'mistral-env';
1132+
process.env.GROQ_API_KEY = 'groq-env';
1133+
1134+
mockCore.getInput.mockImplementation((name: string) => {
1135+
const inputs: Record<string, string> = {
1136+
'github-token': 'mock-github-token',
1137+
config: 'promptfooconfig.yaml',
1138+
prompts: 'prompts/*.txt',
1139+
};
1140+
return inputs[name] || '';
1141+
});
1142+
1143+
await run();
1144+
1145+
const envPassedToExec = mockExec.exec.mock.calls[0][2] as {
1146+
env: Record<string, string>;
1147+
};
1148+
expect(envPassedToExec.env.OPENAI_API_KEY).toBe('openai-env');
1149+
expect(envPassedToExec.env.AZURE_OPENAI_API_KEY).toBe('azure-env');
1150+
expect(envPassedToExec.env.ANTHROPIC_API_KEY).toBe('anthropic-env');
1151+
expect(envPassedToExec.env.HF_API_TOKEN).toBe('hf-env');
1152+
expect(envPassedToExec.env.AWS_ACCESS_KEY_ID).toBe('aws-key-id-env');
1153+
expect(envPassedToExec.env.AWS_SECRET_ACCESS_KEY).toBe('aws-secret-env');
1154+
expect(envPassedToExec.env.REPLICATE_API_KEY).toBe('replicate-env');
1155+
expect(envPassedToExec.env.PALM_API_KEY).toBe('palm-env');
1156+
expect(envPassedToExec.env.VERTEX_API_KEY).toBe('vertex-env');
1157+
expect(envPassedToExec.env.COHERE_API_KEY).toBe('cohere-env');
1158+
expect(envPassedToExec.env.MISTRAL_API_KEY).toBe('mistral-env');
1159+
expect(envPassedToExec.env.GROQ_API_KEY).toBe('groq-env');
1160+
});
1161+
1162+
test('should mix inputs and env vars correctly', async () => {
1163+
process.env.OPENAI_API_KEY = 'openai-env';
1164+
process.env.ANTHROPIC_API_KEY = 'anthropic-env';
1165+
1166+
mockCore.getInput.mockImplementation((name: string) => {
1167+
const inputs: Record<string, string> = {
1168+
'github-token': 'mock-github-token',
1169+
config: 'promptfooconfig.yaml',
1170+
prompts: 'prompts/*.txt',
1171+
'openai-api-key': 'openai-input', // Override with input
1172+
// anthropic-api-key not provided, should use env
1173+
};
1174+
return inputs[name] || '';
1175+
});
1176+
1177+
await run();
1178+
1179+
const envPassedToExec = mockExec.exec.mock.calls[0][2] as {
1180+
env: Record<string, string>;
1181+
};
1182+
expect(envPassedToExec.env.OPENAI_API_KEY).toBe('openai-input'); // Input wins
1183+
expect(envPassedToExec.env.ANTHROPIC_API_KEY).toBe('anthropic-env'); // Env fallback
1184+
});
1185+
});
1186+
1187+
describe('environment variable documentation', () => {
1188+
test('README.md should document environment variable fallback', async () => {
1189+
const path = require('path');
1190+
const realFs = jest.requireActual('fs') as typeof fs;
1191+
1192+
const readmePath = path.join(__dirname, '..', 'README.md');
1193+
const readmeContent = realFs.readFileSync(readmePath, 'utf8');
1194+
1195+
// Check that environment variable section exists
1196+
expect(readmeContent).toContain('### Environment Variables');
1197+
expect(readmeContent).toContain(
1198+
'All workflow environment variables are passed through to promptfoo',
1199+
);
1200+
expect(readmeContent).toContain('Action inputs take precedence');
1201+
});
1202+
1203+
test('action.yml should mention environment variable fallback in descriptions', async () => {
1204+
const yaml = require('js-yaml');
1205+
const path = require('path');
1206+
const realFs = jest.requireActual('fs') as typeof fs;
1207+
1208+
const actionYmlPath = path.join(__dirname, '..', 'action.yml');
1209+
const actionYml = realFs.readFileSync(actionYmlPath, 'utf8');
1210+
const action = yaml.load(actionYml);
1211+
1212+
// Check that API key descriptions mention env var fallback
1213+
expect(action.inputs['openai-api-key'].description).toContain(
1214+
'OPENAI_API_KEY environment variable',
1215+
);
1216+
expect(action.inputs['azure-api-key'].description).toContain(
1217+
'AZURE_OPENAI_API_KEY environment variable',
1218+
);
1219+
expect(action.inputs['anthropic-api-key'].description).toContain(
1220+
'ANTHROPIC_API_KEY environment variable',
1221+
);
1222+
expect(action.inputs['huggingface-api-key'].description).toContain(
1223+
'HF_API_TOKEN environment variable',
1224+
);
1225+
expect(action.inputs['aws-access-key-id'].description).toContain(
1226+
'AWS_ACCESS_KEY_ID environment variable',
1227+
);
1228+
expect(action.inputs['aws-secret-access-key'].description).toContain(
1229+
'AWS_SECRET_ACCESS_KEY environment variable',
1230+
);
1231+
});
1232+
1233+
test('main.ts should have comments explaining fallback behavior', async () => {
1234+
const path = require('path');
1235+
const realFs = jest.requireActual('fs') as typeof fs;
1236+
1237+
const mainPath = path.join(__dirname, '..', 'src', 'main.ts');
1238+
const mainContent = realFs.readFileSync(mainPath, 'utf8');
1239+
1240+
// Check that code has explanatory comments
1241+
expect(mainContent).toContain(
1242+
'Environment variables from workflow context (process.env) are used as fallback',
1243+
);
1244+
expect(mainContent).toContain(
1245+
'Action inputs (if provided) take precedence and override environment variables',
1246+
);
1247+
});
1248+
});

action.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,40 +34,40 @@ inputs:
3434
description: 'Path to cache directory'
3535
required: false
3636
openai-api-key:
37-
description: 'OpenAI API Key'
37+
description: 'OpenAI API Key. Falls back to OPENAI_API_KEY environment variable if not provided.'
3838
required: false
3939
azure-api-key:
40-
description: 'Azure API key'
40+
description: 'Azure API key. Falls back to AZURE_OPENAI_API_KEY environment variable if not provided.'
4141
required: false
4242
anthropic-api-key:
43-
description: 'Anthropic API key'
43+
description: 'Anthropic API key. Falls back to ANTHROPIC_API_KEY environment variable if not provided.'
4444
required: false
4545
huggingface-api-key:
46-
description: 'Huggingface API key'
46+
description: 'Hugging Face API key. Falls back to HF_API_TOKEN environment variable if not provided.'
4747
required: false
4848
aws-access-key-id:
49-
description: 'AWS Access Key ID'
49+
description: 'AWS Access Key ID. Falls back to AWS_ACCESS_KEY_ID environment variable if not provided.'
5050
required: false
5151
aws-secret-access-key:
52-
description: 'AWS Secret Access Key'
52+
description: 'AWS Secret Access Key. Falls back to AWS_SECRET_ACCESS_KEY environment variable if not provided.'
5353
required: false
5454
replicate-api-key:
55-
description: 'Replicate API key'
55+
description: 'Replicate API key. Falls back to REPLICATE_API_KEY environment variable if not provided.'
5656
required: false
5757
palm-api-key:
58-
description: 'Palm API key'
58+
description: 'Palm API key. Falls back to PALM_API_KEY environment variable if not provided.'
5959
required: false
6060
vertex-api-key:
61-
description: 'Google vertex API key'
61+
description: 'Google Vertex API key. Falls back to VERTEX_API_KEY environment variable if not provided.'
6262
required: false
6363
cohere-api-key:
64-
description: 'Cohere API key'
64+
description: 'Cohere API key. Falls back to COHERE_API_KEY environment variable if not provided.'
6565
required: false
6666
mistral-api-key:
67-
description: 'Mistral API key'
67+
description: 'Mistral API key. Falls back to MISTRAL_API_KEY environment variable if not provided.'
6868
required: false
6969
groq-api-key:
70-
description: 'Groq API key'
70+
description: 'Groq API key. Falls back to GROQ_API_KEY environment variable if not provided.'
7171
required: false
7272
promptfoo-version:
7373
description: 'Version of promptfoo to use'

dist/index.js

Lines changed: 3 additions & 1 deletion
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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,9 +495,12 @@ export async function run(): Promise<void> {
495495
promptfooArgs.push('--no-cache');
496496
}
497497

498-
// Build environment with process.env which now includes cache settings from setupCacheEnvironment()
498+
// Build environment for promptfoo execution
499+
// Environment variables from workflow context (process.env) are used as fallback for API keys.
500+
// Action inputs (if provided) take precedence and override environment variables.
499501
const env = {
500-
...process.env, // This now includes all PROMPTFOO_CACHE_* variables set by setupCacheEnvironment()
502+
...process.env, // Includes cache settings and environment variable fallbacks
503+
// Override with action inputs if provided (takes precedence over env vars)
501504
...(openaiApiKey ? { OPENAI_API_KEY: openaiApiKey } : {}),
502505
...(azureApiKey ? { AZURE_OPENAI_API_KEY: azureApiKey } : {}),
503506
...(anthropicApiKey ? { ANTHROPIC_API_KEY: anthropicApiKey } : {}),

0 commit comments

Comments
 (0)