mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 05:35:41 +00:00
feat: allow collection environment and environment file to be used together in run command (#6784)
This commit is contained in:
@@ -354,50 +354,63 @@ const handler = async function (argv) {
|
||||
const runtimeVariables = {};
|
||||
let envVars = {};
|
||||
|
||||
if (env && envFile) {
|
||||
console.error(chalk.red(`Cannot use both --env and --env-file options together`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_MALFORMED_ENV_OVERRIDE);
|
||||
}
|
||||
// Helper to load environment variables from a file
|
||||
const loadEnvFromFile = (filePath, nameOverride) => {
|
||||
const fileExt = path.extname(filePath).toLowerCase();
|
||||
let result = {};
|
||||
|
||||
if (envFile || env) {
|
||||
const envExt = FORMAT_CONFIG[collection.format].ext;
|
||||
const envFilePath = envFile
|
||||
? path.resolve(collectionPath, envFile)
|
||||
: path.join(collectionPath, 'environments', `${env}${envExt}`);
|
||||
|
||||
const envFileExists = await exists(envFilePath);
|
||||
if (!envFileExists) {
|
||||
const errorPath = envFile || `environments/${env}${envExt}`;
|
||||
console.error(chalk.red(`Environment file not found: `) + chalk.dim(errorPath));
|
||||
|
||||
process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
|
||||
if (fileExt === '.json') {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(content);
|
||||
const normalizedEnv = parseEnvironmentJson(parsed);
|
||||
result = getEnvVars(normalizedEnv);
|
||||
const rawName = normalizedEnv?.name;
|
||||
const trimmedName = typeof rawName === 'string' ? rawName.trim() : '';
|
||||
result.__name__ = trimmedName || path.basename(filePath, '.json');
|
||||
} else if (fileExt === '.yml' || fileExt === '.yaml') {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const envJson = parseEnvironment(content, { format: 'yml' });
|
||||
result = getEnvVars(envJson);
|
||||
result.__name__ = nameOverride || path.basename(filePath, fileExt);
|
||||
} else {
|
||||
const content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const envJson = parseEnvironment(content);
|
||||
result = getEnvVars(envJson);
|
||||
result.__name__ = nameOverride || path.basename(filePath, '.bru');
|
||||
}
|
||||
|
||||
const fileExt = path.extname(envFilePath).toLowerCase();
|
||||
if (fileExt === '.json') {
|
||||
let envJsonContent;
|
||||
try {
|
||||
envJsonContent = fs.readFileSync(envFilePath, 'utf8');
|
||||
const parsed = JSON.parse(envJsonContent);
|
||||
const normalizedEnv = parseEnvironmentJson(parsed);
|
||||
envVars = getEnvVars(normalizedEnv);
|
||||
const rawName = normalizedEnv?.name;
|
||||
const trimmedName = typeof rawName === 'string' ? rawName.trim() : '';
|
||||
envVars.__name__ = trimmedName || path.basename(envFilePath, '.json');
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Failed to parse Environment JSON: ${err.message}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
|
||||
}
|
||||
} else if (fileExt === '.yml' || fileExt === '.yaml') {
|
||||
const envContent = fs.readFileSync(envFilePath, 'utf8');
|
||||
const envJson = parseEnvironment(envContent, { format: 'yml' });
|
||||
envVars = getEnvVars(envJson);
|
||||
envVars.__name__ = envFile ? path.basename(envFilePath, fileExt) : env;
|
||||
} else {
|
||||
const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const envJson = parseEnvironment(envBruContent);
|
||||
envVars = getEnvVars(envJson);
|
||||
envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env;
|
||||
return result;
|
||||
};
|
||||
|
||||
// Load --env-file if provided
|
||||
if (envFile) {
|
||||
const envFilePath = path.resolve(collectionPath, envFile);
|
||||
if (!(await exists(envFilePath))) {
|
||||
console.error(chalk.red(`Environment file not found: `) + chalk.dim(envFile));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
envVars = loadEnvFromFile(envFilePath);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Failed to parse environment file: ${err.message}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
// Load --env and merge (collection env takes precedence)
|
||||
if (env) {
|
||||
const envExt = FORMAT_CONFIG[collection.format].ext;
|
||||
const collectionEnvFilePath = path.join(collectionPath, 'environments', `${env}${envExt}`);
|
||||
if (!(await exists(collectionEnvFilePath))) {
|
||||
console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}${envExt}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
const collectionEnvVars = loadEnvFromFile(collectionEnvFilePath, env);
|
||||
envVars = { ...envVars, ...collectionEnvVars };
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Failed to parse Environment file: ${err.message}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
114
tests/runner/cli-env-combined/cli-env-combined.spec.ts
Normal file
114
tests/runner/cli-env-combined/cli-env-combined.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('CLI Combined Environment Support (--env and --env-file)', () => {
|
||||
const collectionPath = path.resolve(__dirname, 'collection');
|
||||
const BRU = 'node ../../../../packages/bruno-cli/bin/bru.js';
|
||||
|
||||
// Helper: run bru CLI and return exit code
|
||||
const runFrom = (cwd: string, args: string): number => {
|
||||
try {
|
||||
execSync(`cd "${cwd}" && ${BRU} ${args}`, { stdio: 'pipe' });
|
||||
return 0;
|
||||
} catch (error: any) {
|
||||
return error?.status ?? 1;
|
||||
}
|
||||
};
|
||||
|
||||
test('CLI: Should allow --env and --env-file to be used together', async () => {
|
||||
const envFilePath = path.join(collectionPath, 'global-env.json');
|
||||
const outputPath = path.join(collectionPath, 'combined-out.json');
|
||||
|
||||
// This should NOT error out anymore - both options should be accepted
|
||||
runFrom(
|
||||
collectionPath,
|
||||
`run request.bru --env CollectionEnv --env-file "${envFilePath}" --reporter-json "${outputPath}"`
|
||||
);
|
||||
|
||||
// Check that the output file was created (command ran successfully)
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(outputPath);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
test('CLI: Collection env (--env) should override env-file variables', async () => {
|
||||
const envFilePath = path.join(collectionPath, 'global-env.json');
|
||||
const outputPath = path.join(collectionPath, 'override-out.json');
|
||||
|
||||
runFrom(
|
||||
collectionPath,
|
||||
`run request.bru --env CollectionEnv --env-file "${envFilePath}" --reporter-json "${outputPath}"`
|
||||
);
|
||||
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
const result = report.results[0];
|
||||
|
||||
// baseUrl should be from collection env (https://echo.usebruno.com), not global (https://global.example.com)
|
||||
expect(result.request.url).toBe('https://echo.usebruno.com');
|
||||
|
||||
// overrideVar should be from collection env, not global
|
||||
const body = JSON.parse(result.request.data);
|
||||
expect(body.overrideVar).toBe('collection-value');
|
||||
|
||||
// globalOnly should come from env-file since it's not in collection env
|
||||
expect(body.globalOnly).toBe('from-global');
|
||||
|
||||
// collectionOnly should come from collection env
|
||||
expect(body.collectionOnly).toBe('from-collection');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(outputPath);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
test('CLI: --env-file only should still work', async () => {
|
||||
const envFilePath = path.join(collectionPath, 'global-env.json');
|
||||
const outputPath = path.join(collectionPath, 'envfile-only-out.json');
|
||||
|
||||
runFrom(collectionPath, `run request.bru --env-file "${envFilePath}" --reporter-json "${outputPath}"`);
|
||||
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
const result = report.results[0];
|
||||
|
||||
// Should use env-file values when --env is not provided
|
||||
// baseUrl would be from global-env.json but the request would fail since it's not a real URL
|
||||
// We just verify the interpolation happened
|
||||
expect(result.request.url).toBe('https://global.example.com');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(outputPath);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
test('CLI: --env only should still work', async () => {
|
||||
const outputPath = path.join(collectionPath, 'env-only-out.json');
|
||||
|
||||
runFrom(collectionPath, `run request.bru --env CollectionEnv --reporter-json "${outputPath}"`);
|
||||
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
const result = report.results[0];
|
||||
|
||||
// Should use collection env values
|
||||
expect(result.request.url).toBe('https://echo.usebruno.com');
|
||||
|
||||
const body = JSON.parse(result.request.data);
|
||||
expect(body.overrideVar).toBe('collection-value');
|
||||
expect(body.collectionOnly).toBe('from-collection');
|
||||
// globalOnly is not in collection env, so it should remain as template
|
||||
expect(body.globalOnly).toBe('{{globalOnly}}');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(outputPath);
|
||||
} catch (_) {}
|
||||
});
|
||||
});
|
||||
5
tests/runner/cli-env-combined/collection/bruno.json
Normal file
5
tests/runner/cli-env-combined/collection/bruno.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "cli-env-combined-test",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
vars {
|
||||
baseUrl: https://echo.usebruno.com
|
||||
overrideVar: collection-value
|
||||
collectionOnly: from-collection
|
||||
}
|
||||
8
tests/runner/cli-env-combined/collection/global-env.json
Normal file
8
tests/runner/cli-env-combined/collection/global-env.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "GlobalEnv",
|
||||
"variables": [
|
||||
{ "name": "baseUrl", "value": "https://global.example.com", "enabled": true },
|
||||
{ "name": "overrideVar", "value": "global-value", "enabled": true },
|
||||
{ "name": "globalOnly", "value": "from-global", "enabled": true }
|
||||
]
|
||||
}
|
||||
19
tests/runner/cli-env-combined/collection/request.bru
Normal file
19
tests/runner/cli-env-combined/collection/request.bru
Normal file
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: combined-env-test
|
||||
type: http
|
||||
}
|
||||
|
||||
http {
|
||||
method: POST
|
||||
url: {{baseUrl}}
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"overrideVar": "{{overrideVar}}",
|
||||
"globalOnly": "{{globalOnly}}",
|
||||
"collectionOnly": "{{collectionOnly}}"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user