From 0ade2632e7b457f77d76d1ecc496e49967fd2483 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:42:29 -0700 Subject: [PATCH 1/2] Test logging on Bash 4.2 --- .github/workflows/tests.yml | 31 +++++- README.md | 4 + tests/bash-42-logging-smoke.sh | 198 +++++++++++++++++++++++++++++++++ tests/lint-warnings.sh | 1 + tests/validate.sh | 4 + 5 files changed, 235 insertions(+), 3 deletions(-) create mode 100755 tests/bash-42-logging-smoke.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1a7f6ac..d7c9a81 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,9 +39,9 @@ jobs: check_bash_version ' bash "$PWD/lib/bash/std/lib_std.sh" - # Hosted runners do not provide Bash 4.2 directly. The BATS suite covers - # the lower-bound comparison logic; this macOS smoke exercises the known - # unsupported system Bash 3.2 path when it is available. + # The exact supported lower bound runs in the bash-42-logging job. This + # macOS smoke exercises the known unsupported system Bash 3.2 path when + # it is available. - name: Smoke unsupported macOS system Bash if: runner.os == 'macOS' run: | @@ -58,3 +58,28 @@ jobs: test "$status" -ne 0 grep -F "requires Bash 4.2 or higher" <<<"$output" ! grep -F "syntax error" <<<"$output" + + bash-42-logging: + name: Logging smoke (Bash 4.2.53) + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - name: Run Bash 4.2 logging smoke + run: | + docker run --rm \ + --platform linux/amd64 \ + --network none \ + --read-only \ + --cap-drop ALL \ + --security-opt no-new-privileges=true \ + --pids-limit 64 \ + --user 65534:65534 \ + --tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m,mode=1777 \ + --env HOME=/tmp \ + --env TMPDIR=/tmp \ + --mount "type=bind,src=$GITHUB_WORKSPACE,dst=/workspace,readonly" \ + --workdir /workspace \ + docker.io/library/bash@sha256:0931edd3941d0603cb3d5da1cb298cf3eb6a579e09e094c3e34e2d5e9df8cddc \ + bash tests/bash-42-logging-smoke.sh 4 2 53 diff --git a/README.md b/README.md index 755f751..98e7163 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,10 @@ The suite expects `bats` and `shellcheck` to be installed. On macOS: brew install bats-core shellcheck ``` +Local validation runs the logging compatibility smoke on the installed +supported Bash. CI runs the same script on the exact minimum runtime, Bash +4.2.53, using a digest-pinned Docker Official Image. + ## Base This repository is managed by [Base](https://github.com/basefoundry/base). diff --git a/tests/bash-42-logging-smoke.sh b/tests/bash-42-logging-smoke.sh new file mode 100755 index 0000000..5824d2a --- /dev/null +++ b/tests/bash-42-logging-smoke.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash + +smoke_fail() { + printf 'Bash logging smoke failed: %s\n' "$*" >&2 + return 1 +} + +smoke_assert_contains() { + local text="$1" expected="$2" label="$3" + + if [[ "$text" != *"$expected"* ]]; then + smoke_fail "$label did not contain '$expected'." + return 1 + fi + return 0 +} + +smoke_file_mode() { + local file_path="$1" mode + + if mode="$(stat -c '%a' "$file_path" 2>/dev/null)"; then + printf '%s' "$mode" + return 0 + fi + stat -f '%Lp' "$file_path" +} + +main() { + local expected_major="${1-}" expected_minor="${2-}" expected_patch="${3-}" + local script_dir repo_root smoke_dir primary_log payload_file + local info_output debug_output terminal_output primary_content mode + local utc_output verbose_output + + if (($# != 0 && $# != 3)); then + smoke_fail "usage: $0 [expected-major expected-minor expected-patch]" + return 1 + fi + + if (($# == 3)) && + [[ "${BASH_VERSINFO[0]}" != "$expected_major" || + "${BASH_VERSINFO[1]}" != "$expected_minor" || + "${BASH_VERSINFO[2]}" != "$expected_patch" ]]; then + smoke_fail "expected Bash $expected_major.$expected_minor.$expected_patch; running $BASH_VERSION." + return 1 + fi + + script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" || { + smoke_fail "unable to resolve the tests directory." + return 1 + } + repo_root="$(cd -- "$script_dir/.." && pwd -P)" || { + smoke_fail "unable to resolve the repository root." + return 1 + } + + unset BASE_CLI_PRIMARY_LOG LOG_DEBUG LOG_UTC NO_COLOR + # shellcheck source=../lib/bash/std/lib_std.sh + if ! source "$repo_root/lib/bash/std/lib_std.sh"; then + smoke_fail "unable to source lib_std.sh." + return 1 + fi + + if ! check_bash_version; then + smoke_fail "the running Bash did not satisfy the supported version check." + return 1 + fi + + info_output="$(log_info -l base_bash_libs.smoke "default info" 2>&1)" + smoke_assert_contains "$info_output" "INFO" "default INFO record" || return 1 + smoke_assert_contains "$info_output" "default info" "default INFO message" || return 1 + + debug_output="$(log_debug -l base_bash_libs.smoke "hidden library debug" 2>&1)" + if [[ -n "$debug_output" ]]; then + smoke_fail "library DEBUG was visible at the default thresholds." + return 1 + fi + if log_is_enabled -l base_bash_libs.smoke DEBUG; then + smoke_fail "log_is_enabled accepted library DEBUG at the default thresholds." + return 1 + fi + + set_log_level DEBUG || { + smoke_fail "unable to enable terminal DEBUG." + return 1 + } + if log_is_enabled -l base_bash_libs.smoke DEBUG; then + smoke_fail "terminal DEBUG bypassed the base_bash_libs INFO category gate." + return 1 + fi + + set_log_category_level -l base_bash_libs.smoke DEBUG || { + smoke_fail "unable to enable the smoke DEBUG category." + return 1 + } + if ! log_is_enabled -l base_bash_libs.smoke.child DEBUG; then + smoke_fail "a child category did not inherit its parent DEBUG gate." + return 1 + fi + debug_output="$(log_debug -l base_bash_libs.smoke.child "visible category debug" 2>&1)" + smoke_assert_contains "$debug_output" "DEBUG" "enabled DEBUG record" || return 1 + smoke_assert_contains "$debug_output" "visible category debug" "enabled DEBUG message" || return 1 + + set_log_level INFO || { + smoke_fail "unable to restore terminal INFO." + return 1 + } + std_make_temp_dir smoke_dir bash42-logging-smoke || { + smoke_fail "unable to create the smoke workspace." + return 1 + } + primary_log="$smoke_dir/primary.log" + payload_file="$smoke_dir/payload.txt" + export BASE_CLI_PRIMARY_LOG="$primary_log" + + if ! log_is_enabled -l base_bash_libs.smoke.child DEBUG; then + smoke_fail "the eligible primary sink did not enable accepted DEBUG." + return 1 + fi + if [[ -e "$primary_log" ]]; then + smoke_fail "log_is_enabled modified the primary sink." + return 1 + fi + + terminal_output="$(log_debug -l base_bash_libs.smoke.child "persisted debug" 2>&1)" + if [[ -n "$terminal_output" ]]; then + smoke_fail "persistent DEBUG leaked to the INFO terminal." + return 1 + fi + primary_content="$(<"$primary_log")" + smoke_assert_contains "$primary_content" "DEBUG" "persistent DEBUG record" || return 1 + smoke_assert_contains "$primary_content" "persisted debug" "persistent DEBUG message" || return 1 + mode="$(smoke_file_mode "$primary_log")" || { + smoke_fail "unable to read the primary-log mode." + return 1 + } + if [[ "$mode" != "600" ]]; then + smoke_fail "primary log mode was $mode instead of 600." + return 1 + fi + + set_log_category_level -l base_bash_libs.smoke INFO || { + smoke_fail "unable to restore the smoke INFO category." + return 1 + } + if log_is_enabled -l base_bash_libs.smoke.child DEBUG; then + smoke_fail "the INFO category gate did not suppress persistent DEBUG." + return 1 + fi + log_debug -l base_bash_libs.smoke.child "blocked persistent debug" 2>&1 + primary_content="$(<"$primary_log")" + if [[ "$primary_content" == *"blocked persistent debug"* ]]; then + smoke_fail "the INFO category gate allowed a persistent DEBUG record." + return 1 + fi + + set_log_category_level -l base_bash_libs.smoke DEBUG || { + smoke_fail "unable to re-enable the smoke DEBUG category." + return 1 + } + printf 'persistent file contents\n' >"$payload_file" || { + smoke_fail "unable to create the file-logging payload." + return 1 + } + terminal_output="$(log_debug_file -l base_bash_libs.smoke.child "$payload_file" 2>&1)" + if [[ -n "$terminal_output" ]]; then + smoke_fail "persistent DEBUG file contents leaked to the INFO terminal." + return 1 + fi + primary_content="$(<"$primary_log")" + smoke_assert_contains "$primary_content" "Contents of file '$payload_file':" \ + "persistent file header" || return 1 + smoke_assert_contains "$primary_content" "persistent file contents" \ + "persistent file payload" || return 1 + + export LOG_UTC=1 + utc_output="$(log_info -l base_bash_libs.smoke "utc info" 2>&1)" + smoke_assert_contains "$utc_output" " UTC INFO" "UTC log timestamp" || return 1 + + set_log_level VERBOSE || { + smoke_fail "unable to enable VERBOSE compatibility." + return 1 + } + set_log_category_level -l base_bash_libs.smoke VERBOSE || { + smoke_fail "unable to enable the VERBOSE compatibility category." + return 1 + } + verbose_output="$(log_verbose -l base_bash_libs.smoke "compat verbose" 2>&1)" + smoke_assert_contains "$verbose_output" "VERBOSE" "VERBOSE compatibility record" || return 1 + if [[ "$verbose_output" == *"deprecated"* ]]; then + smoke_fail "VERBOSE compatibility emitted a runtime deprecation warning." + return 1 + fi + + printf 'Bash logging smoke passed on Bash %s.\n' "$BASH_VERSION" + return 0 +} + +main "$@" diff --git a/tests/lint-warnings.sh b/tests/lint-warnings.sh index 1404f1c..aa9d877 100755 --- a/tests/lint-warnings.sh +++ b/tests/lint-warnings.sh @@ -20,6 +20,7 @@ run_stage() { lint_files=( bin/base-bash + tests/bash-42-logging-smoke.sh tests/validate.sh tests/lint-warnings.sh examples/std-usage.sh diff --git a/tests/validate.sh b/tests/validate.sh index 5600346..4779da8 100755 --- a/tests/validate.sh +++ b/tests/validate.sh @@ -15,6 +15,7 @@ required_files=( .github/workflows/project-intake.yml .github/workflows/tests.yml bin/base-bash + tests/bash-42-logging-smoke.sh examples/std-usage.sh examples/cookbook-cleanup-temp.sh examples/cookbook-args-lists-strings.sh @@ -63,6 +64,7 @@ check_no_strict_mode() { local file matches status local strict_mode_files=( bin/base-bash + tests/bash-42-logging-smoke.sh tests/validate.sh tests/lint-warnings.sh examples/*.sh @@ -191,6 +193,7 @@ done run_stage "ShellCheck error profile" shellcheck --severity=error \ bin/base-bash \ + tests/bash-42-logging-smoke.sh \ tests/validate.sh \ tests/lint-warnings.sh \ examples/std-usage.sh \ @@ -220,6 +223,7 @@ bats_files=( run_stage "BATS test suites" bats \ "${bats_files[@]}" || exit $? +run_stage "Bash logging smoke" tests/bash-42-logging-smoke.sh || exit $? run_stage "examples/std-usage.sh" examples/std-usage.sh >/dev/null || exit $? run_stage "examples/cookbook-cleanup-temp.sh" examples/cookbook-cleanup-temp.sh >/dev/null || exit $? run_stage "examples/cookbook-args-lists-strings.sh" examples/cookbook-args-lists-strings.sh >/dev/null || exit $? From fc50066f4b3ae1c1bf4e5486e580f049b465686a Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:47:17 -0700 Subject: [PATCH 2/2] Tighten Bash 4.2 smoke assertions --- .github/workflows/tests.yml | 11 +++++- tests/bash-42-logging-smoke.sh | 67 +++++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d7c9a81..673a6a8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,9 @@ on: - main pull_request: +permissions: + contents: read + jobs: validate: name: Validate (${{ matrix.os }}) @@ -17,7 +20,9 @@ jobs: - macos-latest - ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Install validation tools (macOS) if: runner.os == 'macOS' @@ -64,7 +69,9 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Run Bash 4.2 logging smoke run: | diff --git a/tests/bash-42-logging-smoke.sh b/tests/bash-42-logging-smoke.sh index 5824d2a..b2889e4 100755 --- a/tests/bash-42-logging-smoke.sh +++ b/tests/bash-42-logging-smoke.sh @@ -15,6 +15,22 @@ smoke_assert_contains() { return 0 } +smoke_capture_stderr() { + local stdout_path="$1" stderr_path="$2" label="$3" + shift 3 + + if ! "$@" >"$stdout_path" 2>"$stderr_path"; then + smoke_fail "$label returned a failure status." + return 1 + fi + if [[ -s "$stdout_path" ]]; then + smoke_fail "$label wrote to stdout instead of reserving it for program output." + return 1 + fi + SMOKE_CAPTURED_STDERR="$(<"$stderr_path")" + return 0 +} + smoke_file_mode() { local file_path="$1" mode @@ -28,6 +44,7 @@ smoke_file_mode() { main() { local expected_major="${1-}" expected_minor="${2-}" expected_patch="${3-}" local script_dir repo_root smoke_dir primary_log payload_file + local terminal_stdout terminal_stderr local info_output debug_output terminal_output primary_content mode local utc_output verbose_output @@ -65,11 +82,24 @@ main() { return 1 fi - info_output="$(log_info -l base_bash_libs.smoke "default info" 2>&1)" + std_make_temp_dir smoke_dir bash42-logging-smoke || { + smoke_fail "unable to create the smoke workspace." + return 1 + } + terminal_stdout="$smoke_dir/terminal.stdout" + terminal_stderr="$smoke_dir/terminal.stderr" + primary_log="$smoke_dir/primary.log" + payload_file="$smoke_dir/payload.txt" + + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "default INFO" \ + log_info -l base_bash_libs.smoke "default info" || return 1 + info_output="$SMOKE_CAPTURED_STDERR" smoke_assert_contains "$info_output" "INFO" "default INFO record" || return 1 smoke_assert_contains "$info_output" "default info" "default INFO message" || return 1 - debug_output="$(log_debug -l base_bash_libs.smoke "hidden library debug" 2>&1)" + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "default DEBUG" \ + log_debug -l base_bash_libs.smoke "hidden library debug" || return 1 + debug_output="$SMOKE_CAPTURED_STDERR" if [[ -n "$debug_output" ]]; then smoke_fail "library DEBUG was visible at the default thresholds." return 1 @@ -96,7 +126,9 @@ main() { smoke_fail "a child category did not inherit its parent DEBUG gate." return 1 fi - debug_output="$(log_debug -l base_bash_libs.smoke.child "visible category debug" 2>&1)" + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "enabled DEBUG" \ + log_debug -l base_bash_libs.smoke.child "visible category debug" || return 1 + debug_output="$SMOKE_CAPTURED_STDERR" smoke_assert_contains "$debug_output" "DEBUG" "enabled DEBUG record" || return 1 smoke_assert_contains "$debug_output" "visible category debug" "enabled DEBUG message" || return 1 @@ -104,12 +136,6 @@ main() { smoke_fail "unable to restore terminal INFO." return 1 } - std_make_temp_dir smoke_dir bash42-logging-smoke || { - smoke_fail "unable to create the smoke workspace." - return 1 - } - primary_log="$smoke_dir/primary.log" - payload_file="$smoke_dir/payload.txt" export BASE_CLI_PRIMARY_LOG="$primary_log" if ! log_is_enabled -l base_bash_libs.smoke.child DEBUG; then @@ -121,7 +147,9 @@ main() { return 1 fi - terminal_output="$(log_debug -l base_bash_libs.smoke.child "persisted debug" 2>&1)" + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "persistent DEBUG" \ + log_debug -l base_bash_libs.smoke.child "persisted debug" || return 1 + terminal_output="$SMOKE_CAPTURED_STDERR" if [[ -n "$terminal_output" ]]; then smoke_fail "persistent DEBUG leaked to the INFO terminal." return 1 @@ -146,7 +174,12 @@ main() { smoke_fail "the INFO category gate did not suppress persistent DEBUG." return 1 fi - log_debug -l base_bash_libs.smoke.child "blocked persistent debug" 2>&1 + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "blocked persistent DEBUG" \ + log_debug -l base_bash_libs.smoke.child "blocked persistent debug" || return 1 + if [[ -n "$SMOKE_CAPTURED_STDERR" ]]; then + smoke_fail "the INFO category gate allowed terminal DEBUG." + return 1 + fi primary_content="$(<"$primary_log")" if [[ "$primary_content" == *"blocked persistent debug"* ]]; then smoke_fail "the INFO category gate allowed a persistent DEBUG record." @@ -161,7 +194,9 @@ main() { smoke_fail "unable to create the file-logging payload." return 1 } - terminal_output="$(log_debug_file -l base_bash_libs.smoke.child "$payload_file" 2>&1)" + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "persistent DEBUG file" \ + log_debug_file -l base_bash_libs.smoke.child "$payload_file" || return 1 + terminal_output="$SMOKE_CAPTURED_STDERR" if [[ -n "$terminal_output" ]]; then smoke_fail "persistent DEBUG file contents leaked to the INFO terminal." return 1 @@ -173,7 +208,9 @@ main() { "persistent file payload" || return 1 export LOG_UTC=1 - utc_output="$(log_info -l base_bash_libs.smoke "utc info" 2>&1)" + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "UTC INFO" \ + log_info -l base_bash_libs.smoke "utc info" || return 1 + utc_output="$SMOKE_CAPTURED_STDERR" smoke_assert_contains "$utc_output" " UTC INFO" "UTC log timestamp" || return 1 set_log_level VERBOSE || { @@ -184,7 +221,9 @@ main() { smoke_fail "unable to enable the VERBOSE compatibility category." return 1 } - verbose_output="$(log_verbose -l base_bash_libs.smoke "compat verbose" 2>&1)" + smoke_capture_stderr "$terminal_stdout" "$terminal_stderr" "VERBOSE compatibility" \ + log_verbose -l base_bash_libs.smoke "compat verbose" || return 1 + verbose_output="$SMOKE_CAPTURED_STDERR" smoke_assert_contains "$verbose_output" "VERBOSE" "VERBOSE compatibility record" || return 1 if [[ "$verbose_output" == *"deprecated"* ]]; then smoke_fail "VERBOSE compatibility emitted a runtime deprecation warning."