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
40 changes: 36 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
- main
pull_request:

permissions:
contents: read

jobs:
validate:
name: Validate (${{ matrix.os }})
Expand All @@ -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'
Expand All @@ -39,9 +44,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: |
Expand All @@ -58,3 +63,30 @@ 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@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
237 changes: 237 additions & 0 deletions tests/bash-42-logging-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#!/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_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

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 terminal_stdout terminal_stderr
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

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

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
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
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

set_log_level INFO || {
smoke_fail "unable to restore terminal INFO."
return 1
}
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

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
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
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."
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
}
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
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
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 || {
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
}
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."
return 1
fi

printf 'Bash logging smoke passed on Bash %s.\n' "$BASH_VERSION"
return 0
}

main "$@"
1 change: 1 addition & 0 deletions tests/lint-warnings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions tests/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 $?
Expand Down
Loading