Ctrl K
ComunidadGuías…

updater.sh

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash

set -Euo pipefail
umask 022

PATH="$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH

OSC_ALERT_EMAIL=${OSC_ALERT_EMAIL:-lisakov57@gmail.com}
OSC_PROJECT_DIR=${OSC_PROJECT_DIR:-/home/slisakov/open-source-comments}
OSC_SCRIPT_DIR=${OSC_SCRIPT_DIR:-$OSC_PROJECT_DIR}
OSC_STATE_DIR=${OSC_STATE_DIR:-$HOME/.local/state/open-source-comments}
export OSC_ALERT_EMAIL
export PYTHONPATH="$OSC_SCRIPT_DIR:$OSC_PROJECT_DIR${PYTHONPATH:+:$PYTHONPATH}"

generated_files=(
    apigh/history.json
    data.js
    data.yaml
    index.html
    star-history.js
    stars-v-date.svg
)

current_step="startup"
failure_alert_sent=0
transaction_active=0
backup_dir=""
fallback_lock_dir=""
run_log=""
dns_proxy_pid=""
dns_proxy_port_file=""
dns_fallback_notice_file=""


log_message() {
    printf '%s\n' "$*" >&2
    if command -v logger >/dev/null 2>&1; then
        logger -t open-source-comments-updater -- "$*" || true
    fi
}


send_mail() {
    subject=$1
    body=$2

    if [ -z "$OSC_ALERT_EMAIL" ]; then
        log_message "OSC_ALERT_EMAIL is empty; email alert was not sent"
        return 1
    fi
    if ! command -v mail >/dev/null 2>&1; then
        log_message "mail command is unavailable; email alert was not sent"
        return 1
    fi

    attempt=1
    while [ "$attempt" -le 3 ]; do
        if printf '%s\n' "$body" | mail -s "$subject" "$OSC_ALERT_EMAIL"; then
            return 0
        fi
        log_message "mail delivery attempt ${attempt}/3 failed"
        attempt=$((attempt + 1))
        if [ "$attempt" -le 3 ]; then
            sleep "$attempt"
        fi
    done
    return 1
}


rollback_generation() {
    if [ "$transaction_active" -ne 1 ] || [ -z "$backup_dir" ]; then
        return
    fi

    log_message "Rolling generated files back to their pre-run state"
    git reset --mixed HEAD -- "${generated_files[@]}" >/dev/null 2>&1 || true
    index=0
    for file in "${generated_files[@]}"; do
        backup="$backup_dir/files/$file"
        if [ -f "$backup" ]; then
            mkdir -p "$(dirname "$file")"
            cp -p "$backup" "$file"
        elif [ -f "$backup_dir/missing/$index" ]; then
            rm -f -- "$file"
        fi
        index=$((index + 1))
    done
    transaction_active=0
}


notify_failure() {
    status=$1
    step=$2
    trap - ERR
    failure_alert_sent=1
    rollback_generation

    timestamp=$(date '+%Y-%m-%dT%H:%M:%S%z')
    body="open-source-comments update failed on $(hostname) at ${timestamp}.

Step: ${step}
Exit status: ${status}

The generated files were not deployed."
    if [ -n "$run_log" ] && [ -f "$run_log" ]; then
        recent_output=$(tail -n 50 "$run_log" 2>/dev/null || true)
        body="${body}

Recent output:
${recent_output}"
    fi
    log_message "$body"
    send_mail "open-source-comments update failed: ${step}" "$body" || true
    exit "$status"
}


install_project_scripts() {
    for file in $(git ls-files '*.py'); do
        install -m 700 "$file" "$OSC_SCRIPT_DIR/$file"
    done
    # Bytecode of a module that has since been renamed or deleted would keep
    # importing after its source is gone.
    rm -rf "$OSC_SCRIPT_DIR/__pycache__"
}


check_installed_updater() {
    if cmp -s "$OSC_PROJECT_DIR/updater.sh" "$OSC_SCRIPT_DIR/updater.sh"; then
        return 0
    fi
    log_message "updater.sh in $OSC_SCRIPT_DIR differs from the checkout. Bash is already reading the installed copy, so it cannot be swapped mid-run. Run $OSC_PROJECT_DIR/install_scripts.sh, then this job will pick the new one up."
    return 1
}


run_step() {
    current_step=$1
    shift
    printf '\n[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$current_step"
    "$@" || notify_failure "$?" "$current_step"
}


cleanup() {
    status=$?
    trap - ERR
    if [ "$status" -ne 0 ] && [ "$failure_alert_sent" -eq 0 ]; then
        rollback_generation
    fi
    if [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
        rm -rf -- "$backup_dir"
    fi
    if [ -n "$fallback_lock_dir" ] && [ -d "$fallback_lock_dir" ]; then
        rmdir "$fallback_lock_dir" 2>/dev/null || true
    fi
    if [ -n "$dns_proxy_pid" ]; then
        kill "$dns_proxy_pid" 2>/dev/null || true
        wait "$dns_proxy_pid" 2>/dev/null || true
    fi
    if [ -n "$dns_proxy_port_file" ]; then
        rm -f -- "$dns_proxy_port_file"
    fi
}


unexpected_error() {
    status=$1
    notify_failure "$status" "$current_step"
}


handle_signal() {
    signal=$1
    notify_failure 130 "received signal ${signal} during ${current_step}"
}


trap cleanup EXIT
trap 'unexpected_error $?' ERR
trap 'handle_signal INT' INT
trap 'handle_signal TERM' TERM
trap 'handle_signal HUP' HUP

mkdir -p "$OSC_STATE_DIR"
dns_fallback_notice_file="$OSC_STATE_DIR/dns-fallback-notified"
if command -v flock >/dev/null 2>&1; then
    exec 9>"$OSC_STATE_DIR/updater.lock"
    if ! flock -n 9; then
        body="A second open-source-comments updater tried to start on $(hostname), but another run still holds the lock."
        log_message "$body"
        send_mail "open-source-comments updater overlap" "$body" || true
        exit 75
    fi
else
    fallback_lock_dir="$OSC_STATE_DIR/updater.lock.d"
    if ! mkdir "$fallback_lock_dir" 2>/dev/null; then
        body="A second open-source-comments updater tried to start on $(hostname), but another run still holds the fallback lock."
        log_message "$body"
        send_mail "open-source-comments updater overlap" "$body" || true
        exit 75
    fi
fi

run_log="$OSC_STATE_DIR/last-run.log"
: > "$run_log"
exec > >(tee -a "$run_log") 2>&1

if ! getent ahostsv4 github.com >/dev/null 2>&1; then
    current_step='start DNS fallback proxy'
    dns_proxy_port_file="$OSC_STATE_DIR/dns-proxy.port"
    rm -f -- "$dns_proxy_port_file"
    python3 "$OSC_SCRIPT_DIR/dns_proxy.py" --port-file "$dns_proxy_port_file" &
    dns_proxy_pid=$!

    attempt=1
    while [ "$attempt" -le 20 ] && [ ! -s "$dns_proxy_port_file" ]; do
        if ! kill -0 "$dns_proxy_pid" 2>/dev/null; then
            notify_failure 1 "$current_step"
        fi
        sleep 0.25
        attempt=$((attempt + 1))
    done
    if [ ! -s "$dns_proxy_port_file" ]; then
        notify_failure 1 "$current_step"
    fi

    dns_proxy_port=$(tr -d '[:space:]' < "$dns_proxy_port_file")
    case "$dns_proxy_port" in
        ''|*[!0-9]*) notify_failure 1 "$current_step" ;;
    esac
    HTTPS_PROXY="http://127.0.0.1:$dns_proxy_port"
    HTTP_PROXY="$HTTPS_PROXY"
    https_proxy="$HTTPS_PROXY"
    http_proxy="$HTTPS_PROXY"
    NO_PROXY="127.0.0.1,localhost"
    no_proxy="$NO_PROXY"
    export HTTPS_PROXY HTTP_PROXY https_proxy http_proxy NO_PROXY no_proxy

    body="System DNS is unavailable on $(hostname). The updater activated its local DNS fallback proxy for this run. TLS hostname verification remains enabled."
    log_message "$body"
    if [ ! -e "$dns_fallback_notice_file" ]; then
        if send_mail "open-source-comments DNS fallback activated" "$body"; then
            touch "$dns_fallback_notice_file"
        fi
    fi
else
    rm -f -- "$dns_fallback_notice_file"
fi

cd "$OSC_PROJECT_DIR"

run_step 'verify clean repository before update' test -z "$(git status --porcelain --untracked-files=no)"
run_step 'pull repository with fast-forward only' nice -n5 git pull --ff-only
run_step 'verify clean repository after pull' test -z "$(git status --porcelain --untracked-files=no)"

# The Python below runs from OSC_SCRIPT_DIR rather than from the checkout, so
# that the pull above cannot replace code while it executes. The cost is that
# the two drift: a build step added to the repository is not one this job knows
# about until it is installed. Close that gap here, on every run.
if [ "$OSC_SCRIPT_DIR" != "$OSC_PROJECT_DIR" ]; then
    run_step 'verify the installed updater matches the checkout' check_installed_updater
    run_step 'install scripts for this run' install_project_scripts
fi

backup_dir=$(mktemp -d "${TMPDIR:-/tmp}/open-source-comments-updater.XXXXXX")
mkdir -p "$backup_dir/files" "$backup_dir/missing"
index=0
for file in "${generated_files[@]}"; do
    if [ -f "$file" ]; then
        mkdir -p "$backup_dir/files/$(dirname "$file")"
        cp -p "$file" "$backup_dir/files/$file"
    else
        touch "$backup_dir/missing/$index"
    fi
    index=$((index + 1))
done
transaction_active=1

run_step 'collect repository data' nice -n5 python3 "$OSC_SCRIPT_DIR/get_data.py"
run_step 'render index.html' nice -n5 python3 "$OSC_SCRIPT_DIR/md_to_html.py"
run_step 'generate data.js' nice -n5 python3 "$OSC_SCRIPT_DIR/yaml_2_js.py"
run_step 'generate stars chart' nice -n5 python3 "$OSC_SCRIPT_DIR/plot-stars.py"
run_step 'generate star history' nice -n5 python3 "$OSC_SCRIPT_DIR/history_2_js.py"
run_step 'validate all generated outputs' nice -n5 python3 "$OSC_SCRIPT_DIR/validate_outputs.py"
run_step 'check generated diff for whitespace errors' git diff --check -- "${generated_files[@]}"
run_step 'stage generated data' nice -n5 git add -- "${generated_files[@]}"

if git diff --cached --quiet; then
    echo 'No generated changes to commit'
else
    run_step 'commit generated data' nice -n5 git commit -m 'automatic update'
fi
transaction_active=0

run_step 'push generated data' nice -n5 git push origin HEAD:master

deploy_root=/var/www/lisakov.com/projects/open-source-comments
run_step 'deploy generated files' nice -n5 rsync -a --delay-updates \
    data.js star-history.js index.html stars-v-date.svg "$deploy_root/"
# --delete keeps the deploy root a mirror of the repository. The GoAccess
# reports under /day/ and /week/ list this project's old DataTables URLs, but
# those are access-log entries, not references: no page links to them.
run_step 'deploy CSS' nice -n5 rsync -a --delete-delay --delay-updates \
    css/ "$deploy_root/css/"
run_step 'deploy JavaScript' nice -n5 rsync -a --delete-delay --delay-updates \
    js/ "$deploy_root/js/"
run_step 'verify deployed data.js' cmp -s data.js "$deploy_root/data.js"
run_step 'verify deployed star history' cmp -s star-history.js "$deploy_root/star-history.js"
run_step 'verify deployed index.html' cmp -s index.html "$deploy_root/index.html"
run_step 'verify deployed chart' cmp -s stars-v-date.svg "$deploy_root/stars-v-date.svg"
run_step 'verify clean repository after update' test -z "$(git status --porcelain --untracked-files=no)"

echo 'open-source-comments update completed successfully'