diff --git a/.changeset/remove-ci-trace-auto-enable.md b/.changeset/remove-ci-trace-auto-enable.md new file mode 100644 index 0000000..260f301 --- /dev/null +++ b/.changeset/remove-ci-trace-auto-enable.md @@ -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 diff --git a/examples/reproduce-issue-135-v2.mjs b/examples/reproduce-issue-135-v2.mjs new file mode 100755 index 0000000..da619ca --- /dev/null +++ b/examples/reproduce-issue-135-v2.mjs @@ -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 ===' +); diff --git a/examples/reproduce-issue-135.mjs b/examples/reproduce-issue-135.mjs new file mode 100755 index 0000000..10bc1e5 --- /dev/null +++ b/examples/reproduce-issue-135.mjs @@ -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"} ==='); diff --git a/examples/test-issue-135-comprehensive.mjs b/examples/test-issue-135-comprehensive.mjs new file mode 100755 index 0000000..3f8d574 --- /dev/null +++ b/examples/test-issue-135-comprehensive.mjs @@ -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 ==='); diff --git a/examples/test-trace-option.mjs b/examples/test-trace-option.mjs new file mode 100755 index 0000000..e66aa50 --- /dev/null +++ b/examples/test-trace-option.mjs @@ -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); +} diff --git a/src/$.mjs b/src/$.mjs index 05040a1..445264d 100755 --- a/src/$.mjs +++ b/src/$.mjs @@ -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(); diff --git a/src/$.utils.mjs b/src/$.utils.mjs index d36082e..1dd1a7b 100644 --- a/src/$.utils.mjs +++ b/src/$.utils.mjs @@ -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(); diff --git a/tests/issue-135-final.test.mjs b/tests/issue-135-final.test.mjs new file mode 100755 index 0000000..f7edc02 --- /dev/null +++ b/tests/issue-135-final.test.mjs @@ -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); + }); +});