#!/usr/bin/env bash truthy() { local value value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" case "${value}" in 1 | true | yes | on) return 0 ;; *) return 1 ;; esac } disabled_value() { local value value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" case "${value}" in 0 | false | no | off | disabled) return 0 ;; *) return 1 ;; esac } jeannie_log_start() { local command_name="$1" local timestamp if [[ -n "${JEANNIE_LOG_FILE}" ]]; then return 0 fi timestamp="$(date +%Y%m%d-%H%M%S)" mkdir -p "${JEANNIE_LOG_DIR}" JEANNIE_LOG_FILE="${JEANNIE_LOG_DIR}/jeannie-${command_name}-${timestamp}.log" JEANNIE_ERROR_LOG_FILE="${JEANNIE_LOG_DIR}/jeannie-${command_name}-${timestamp}.error.log" : >"${JEANNIE_LOG_FILE}" : >"${JEANNIE_ERROR_LOG_FILE}" printf 'Log: %s\n' "${JEANNIE_LOG_FILE}" printf 'Error log: %s\n' "${JEANNIE_ERROR_LOG_FILE}" } jeannie_step_plan() { JEANNIE_STEP_INDEX=0 JEANNIE_STEP_TOTAL="$1" } jeannie_verbose_enabled() { truthy "${JEANNIE_VERBOSE:-false}" } jeannie_step_prefix() { local description="$1" if ((JEANNIE_STEP_TOTAL > 0)); then printf '[%d/%d] %-42s ' "${JEANNIE_STEP_INDEX}" "${JEANNIE_STEP_TOTAL}" "${description}" else printf '[%d] %-46s ' "${JEANNIE_STEP_INDEX}" "${description}" fi } jeannie_compact_text() { local text="$1" local max_length="${2:-96}" text="${text//$'\r'/ }" text="${text//$'\n'/ }" text="$(printf '%s' "${text}" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')" if ((${#text} > max_length)); then text="${text:0:max_length - 3}..." fi printf '%s' "${text}" } jeannie_step_progress() { local description="$1" local step_log="$2" local started_at="$3" local last_line="${4:-}" local elapsed=$((SECONDS - started_at)) printf '\r' jeannie_step_prefix "${description}" if [[ -n "${last_line}" ]]; then printf 'running %ss | %s' "${elapsed}" "${last_line}" else printf 'running %ss' "${elapsed}" fi printf '\033[K' } jeannie_step_latest_progress_line() { local step_log="$1" local last_line="" if [[ -s "${step_log}" ]]; then last_line="$(tail -n 1 "${step_log}" 2>/dev/null || true)" last_line="$(jeannie_compact_text "${last_line}" 88)" fi printf '%s' "${last_line}" } redact_sensitive_output() { sed -E \ -e 's/(GITEA_BOOTSTRAP_PASSWORD=)["'\'']?[^"'\''[:space:]]+["'\'']?/\1[REDACTED]/g' \ -e 's/(PIHOLE_WEBPASSWORD=)["'\'']?[^"'\''[:space:]]+["'\'']?/\1[REDACTED]/g' \ -e 's/(COSIGN_PASSWORD=)["'\'']?[^"'\''[:space:]]+["'\'']?/\1[REDACTED]/g' \ -e 's/(GITEA_RUNNER_REGISTRATION_TOKEN=)["'\'']?[^"'\''[:space:]]+["'\'']?/\1[REDACTED]/g' \ -e 's/(--password[=[:space:]]+)["'\'']?[^"'\''[:space:]]+["'\'']?/\1[REDACTED]/g' \ -e 's/(--token[=[:space:]]+)["'\'']?[^"'\''[:space:]]+["'\'']?/\1[REDACTED]/g' \ -e 's/(password[=:][[:space:]]*)[^[:space:]]+/\1[REDACTED]/Ig' \ -e 's/(token[=:][[:space:]]*)[^[:space:]]+/\1[REDACTED]/Ig' \ -e 's#(https?://[^:/[:space:]]+):[^@/[:space:]]+@#\1:[REDACTED]@#g' } run_step() { local description="$1" local step_log local status_file local status local progress_pid local progress_interval="${JEANNIE_PROGRESS_INTERVAL_SECONDS:-10}" local started_at local current_progress_line="" local last_progress_line="" local last_progress_rendered_at=0 shift JEANNIE_STEP_INDEX=$((JEANNIE_STEP_INDEX + 1)) jeannie_step_prefix "${description}" step_log="$(mktemp)" status_file="$(mktemp)" { printf '\n== [%d/%d] %s ==\n' "${JEANNIE_STEP_INDEX}" "${JEANNIE_STEP_TOTAL}" "${description}" printf 'Started: %s\n' "$(date -Is)" } >>"${JEANNIE_LOG_FILE}" if jeannie_verbose_enabled; then set +e "$@" 2>&1 | redact_sensitive_output | tee "${step_log}" | tee -a "${JEANNIE_LOG_FILE}" status=${PIPESTATUS[0]} set -e else set +e ( set +e "$@" 2>&1 | redact_sensitive_output | tee "${step_log}" >>"${JEANNIE_LOG_FILE}" printf '%s\n' "${PIPESTATUS[0]}" >"${status_file}" ) & progress_pid=$! if ! [[ "${progress_interval}" =~ ^[0-9]+$ ]] || ((progress_interval < 1)); then progress_interval=10 fi started_at="${SECONDS}" while kill -0 "${progress_pid}" >/dev/null 2>&1; do sleep "${progress_interval}" if kill -0 "${progress_pid}" >/dev/null 2>&1; then current_progress_line="$(jeannie_step_latest_progress_line "${step_log}")" if [[ "${current_progress_line}" != "${last_progress_line}" ]] || ((SECONDS - last_progress_rendered_at >= 60)); then jeannie_step_progress "${description}" "${step_log}" "${started_at}" "${current_progress_line}" last_progress_line="${current_progress_line}" last_progress_rendered_at="${SECONDS}" fi fi done wait "${progress_pid}" || true status="$(cat "${status_file}" 2>/dev/null || printf '1')" set -e fi printf 'Finished: %s\n' "$(date -Is)" >>"${JEANNIE_LOG_FILE}" if ((status == 0)); then printf '\r' jeannie_step_prefix "${description}" printf 'ok\033[K\n' rm -f "${step_log}" "${status_file}" return 0 fi printf '\r' jeannie_step_prefix "${description}" printf 'failed\033[K\n\n' { printf '\n== FAILED [%d/%d] %s ==\n' "${JEANNIE_STEP_INDEX}" "${JEANNIE_STEP_TOTAL}" "${description}" printf 'Finished: %s\n' "$(date -Is)" sed 's/^/ /' "${step_log}" } >>"${JEANNIE_ERROR_LOG_FILE}" printf 'Step failed: %s\n' "${description}" >&2 printf 'Exit status: %s\n' "${status}" >&2 printf 'Full step output:\n' >&2 sed 's/^/ /' "${step_log}" >&2 printf '\nFull log: %s\n' "${JEANNIE_LOG_FILE}" >&2 printf 'Error log: %s\n' "${JEANNIE_ERROR_LOG_FILE}" >&2 rm -f "${step_log}" "${status_file}" return "${status}" }