Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/remove-ci-trace-auto-enable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'command-stream': patch
---

Fix trace logs interfering with output when CI=true

- Removed automatic trace log enabling when CI environment variable is set
- Trace logs no longer pollute stderr in CI/CD environments (GitHub Actions, GitLab CI, etc.)
- Added COMMAND_STREAM_TRACE environment variable for explicit trace control
- COMMAND_STREAM_TRACE=true explicitly enables tracing
- COMMAND_STREAM_TRACE=false explicitly disables tracing (overrides COMMAND_STREAM_VERBOSE)
- COMMAND_STREAM_VERBOSE=true continues to work as before
- JSON parsing works reliably in CI environments

Fixes #135
15 changes: 15 additions & 0 deletions examples/reproduce-issue-135-v2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node
// Reproducing issue #135: Trace logs interfere with output when CI=true
// This test sets CI=true BEFORE importing the module

process.env.CI = 'true';

import { $ } from '../src/$.mjs';

console.log('=== Test with CI=true set BEFORE import ===');
const $silent = $({ mirror: false, capture: true });
const result = await $silent`echo '{"status":"ok"}'`;
console.log('Output:', result.stdout || result);
console.log(
'\n=== Expected: Should be just {"status":"ok"} without trace logs ==='
);
17 changes: 17 additions & 0 deletions examples/reproduce-issue-135.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env node
// Reproducing issue #135: Trace logs interfere with output when CI=true

import { $ } from '../src/$.mjs';

const $silent = $({ mirror: false, capture: true });

console.log('=== Test 1: Without CI environment ===');
const result1 = await $silent`echo '{"status":"ok"}'`;
console.log('Output:', result1.stdout || result1);

console.log('\n=== Test 2: With CI=true environment ===');
process.env.CI = 'true';
const result2 = await $silent`echo '{"status":"ok"}'`;
console.log('Output:', result2.stdout || result2);

console.log('\n=== Expected: Both outputs should be just {"status":"ok"} ===');
41 changes: 41 additions & 0 deletions examples/test-issue-135-comprehensive.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node
// Comprehensive test for issue #135: Trace logs interfere with output

import { $ } from '../src/$.mjs';

console.log(
'=== Test 1: Default (no env vars, mirror:false, capture:true) ==='
);
const $silent = $({ mirror: false, capture: true });
const result1 = await $silent`echo test1`;
console.log('Output:', result1.stdout);
console.log('Expected: just "test1"\n');

console.log('=== Test 2: CI=true (should NOT produce trace logs) ===');
process.env.CI = 'true';
const $silent2 = $({ mirror: false, capture: true });
const result2 = await $silent2`echo test2`;
console.log('Output:', result2.stdout);
console.log('Expected: just "test2"\n');

console.log(
'=== Test 3: CI=true + COMMAND_STREAM_TRACE=true (should produce trace logs) ==='
);
process.env.COMMAND_STREAM_TRACE = 'true';
const $silent3 = $({ mirror: false, capture: true });
const result3 = await $silent3`echo test3`;
console.log('Output:', result3.stdout);
console.log('Expected: "test3" (trace logs should appear in stderr above)\n');

console.log(
'=== Test 4: COMMAND_STREAM_TRACE=false overrides COMMAND_STREAM_VERBOSE=true ==='
);
process.env.COMMAND_STREAM_VERBOSE = 'true';
process.env.COMMAND_STREAM_TRACE = 'false';
const result4 = await $silent`echo test4`;
console.log('Output:', result4.stdout);
console.log(
'Expected: just "test4" (no trace logs even though VERBOSE=true)\n'
);

console.log('=== All tests completed ===');
21 changes: 21 additions & 0 deletions examples/test-trace-option.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env node
// Test the trace option in $ config

import { $ } from '../src/$.mjs';

console.log('=== Test: mirror:false with trace:false in CI environment ===');
process.env.CI = 'true';

const $silent = $({ mirror: false, capture: true, trace: false });
const result = await $silent`echo '{"status":"ok"}'`;
console.log('JSON Output:', result.stdout);

console.log('\n=== Parsing JSON to verify it works ===');
try {
const parsed = JSON.parse(result.stdout);
console.log('Parsed successfully:', parsed);
console.log('βœ“ Test PASSED: No trace logs interfered with JSON parsing');
} catch (e) {
console.error('βœ— Test FAILED: Could not parse JSON:', e.message);
console.error('Raw output:', result.stdout);
}
24 changes: 20 additions & 4 deletions src/$.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,30 @@ import { parseShellCommand, needsRealShell } from './shell-parser.mjs';

const isBun = typeof globalThis.Bun !== 'undefined';

const VERBOSE =
process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true';

// Trace function for verbose logging
function trace(category, messageOrFunc) {
// Can be controlled via COMMAND_STREAM_VERBOSE or COMMAND_STREAM_TRACE env vars
// Can be disabled per-command via trace: false option
// CI environment no longer auto-enables tracing
function trace(category, messageOrFunc, runner = null) {
// Check if runner explicitly disabled tracing
if (runner && runner.options && runner.options.trace === false) {
return;
}

// Check global trace setting (evaluated dynamically for runtime changes)
const TRACE_ENV = process.env.COMMAND_STREAM_TRACE;
const VERBOSE_ENV = process.env.COMMAND_STREAM_VERBOSE === 'true';

// COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true
// COMMAND_STREAM_TRACE=true explicitly enables tracing
// Otherwise, use COMMAND_STREAM_VERBOSE
const VERBOSE =
TRACE_ENV === 'false' ? false : TRACE_ENV === 'true' ? true : VERBOSE_ENV;

if (!VERBOSE) {
return;
}

const message =
typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
const timestamp = new Date().toISOString();
Expand Down
16 changes: 14 additions & 2 deletions src/$.utils.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import path from 'path';

const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true';

// Trace function for verbose logging - consistent with src/$.mjs
// Can be controlled via COMMAND_STREAM_VERBOSE or COMMAND_STREAM_TRACE env vars
// CI environment no longer auto-enables tracing
export function trace(category, messageOrFunc) {
// Check global trace setting (evaluated dynamically for runtime changes)
const TRACE_ENV = process.env.COMMAND_STREAM_TRACE;
const VERBOSE_ENV = process.env.COMMAND_STREAM_VERBOSE === 'true';

// COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true
// COMMAND_STREAM_TRACE=true explicitly enables tracing
// Otherwise, use COMMAND_STREAM_VERBOSE
const VERBOSE =
TRACE_ENV === 'false' ? false : TRACE_ENV === 'true' ? true : VERBOSE_ENV;

if (!VERBOSE) {
return;
}

const message =
typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
const timestamp = new Date().toISOString();
Expand Down
58 changes: 58 additions & 0 deletions tests/issue-135-final.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Final test for issue #135: CI environment should not auto-enable trace
// This test verifies the main fix: CI=true should NOT cause trace logs
import { describe, it, beforeEach } from 'bun:test';
import assert from 'assert';
import { $ } from '../src/$.mjs';

describe('Issue #135: CI environment no longer auto-enables trace logs', () => {
beforeEach(() => {
// Clean up environment before each test
delete process.env.COMMAND_STREAM_VERBOSE;
delete process.env.COMMAND_STREAM_TRACE;
delete process.env.CI;
});

it('should NOT emit trace logs when CI=true (main fix)', async () => {
process.env.CI = 'true';

const $silent = $({ mirror: false, capture: true });
const result = await $silent`echo '{"status":"ok"}'`;

// Output should be clean JSON without trace logs
assert.strictEqual(result.stdout.trim(), '{"status":"ok"}');

// Should be parseable as JSON
const parsed = JSON.parse(result.stdout);
assert.deepStrictEqual(parsed, { status: 'ok' });
});

it('should allow JSON parsing in CI environment', async () => {
process.env.CI = 'true';

const $silent = $({ mirror: false, capture: true });
const result = await $silent`echo '{"count":42,"items":["a","b","c"]}'`;

// Should be able to parse complex JSON
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.count, 42);
assert.deepStrictEqual(parsed.items, ['a', 'b', 'c']);
});

it('should NOT produce trace logs by default (no env vars)', async () => {
const $silent = $({ mirror: false, capture: true });
const result = await $silent`echo test`;

// Simple text output should be clean
assert.strictEqual(result.stdout.trim(), 'test');
});

it('should work with mirror:false in CI environment', async () => {
process.env.CI = 'true';

const $silent = $({ mirror: false, capture: true });
const result = await $silent`echo hello`;

assert.strictEqual(result.stdout.trim(), 'hello');
assert.strictEqual(result.code, 0);
});
});
Loading