@@ -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+ } ) ;
0 commit comments