#!/usr/bin/env bash # Run `ai-chat-exporter sync` once per provider — the ExecStart of the systemd # unit that install-systemd-timer.sh writes. # # ./scheduling/run-sync.sh claude-code codex # # Every provider is attempted even after one fails, and the exit code is # non-zero if any failed — one ExecStart per provider would stop at the first. # # The app pushes its own ntfy result, but only from the end of a run it # finished. A crash, a non-zero exit before the sync starts (the terms-of-service # gate, a cache error) or a launcher that can't build its venv sends nothing, and # because each provider pushes separately, the providers that did succeed still # send "OK" — so a broken one looks like a quiet day. This script pushes a FAILED # notification for any run that exited non-zero without the app having reported # it. (Its "Sync completed with failures" banner prints right after its push.) # # The push carries the provider, the exit code and, for a crash, the exception's # class name — never its message. Same counts-only rule as src/notify.py: on a # public ntfy topic anyone who guesses the name can read it, and exception text # can carry conversation titles. The full traceback is in the journal. set -uo pipefail REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" LAUNCHER="$REPO/ai-chat-exporter" # NTFY_* as the app resolves them: the environment wins, then .env. env_value() { local name=$1 value=${!1:-} if [ -z "$value" ] && [ -f "$REPO/.env" ]; then value=$(sed -n "s/^[[:space:]]*$name[[:space:]]*=[[:space:]]*//p" "$REPO/.env" | tail -n 1) value=${value%%[[:space:]]#*} value=${value%"${value##*[![:space:]]}"} value=${value#[\"\']} value=${value%[\"\']} fi printf '%s' "$value" } push_failure() { local body=$1 topic server token policy topic=$(env_value NTFY_TOPIC) policy=$(env_value NTFY_NOTIFY | tr '[:upper:]' '[:lower:]') if [ -z "$topic" ] || [ "$policy" = "off" ]; then return 0 fi server=$(env_value NTFY_SERVER) server=${server:-https://ntfy.sh} token=$(env_value NTFY_TOKEN) local args=(-fsS --max-time 15 -o /dev/null -H "Title: AI archive FAILED - $(hostname -s)" -H "Tags: rotating_light" -H "Priority: high" --data-binary "$body") [ -n "$token" ] && args+=(-H "Authorization: Bearer $token") curl "${args[@]}" "${server%/}/$topic" \ || echo "run-sync: could not send the failure notification" >&2 } [ $# -eq 0 ] && set -- all rc=0 for provider in "$@"; do out=$(mktemp) "$LAUNCHER" sync --provider "$provider" --joplin-optional 2>&1 | tee "$out" status=${PIPESTATUS[0]} if [ "$status" -ne 0 ]; then rc=1 if ! grep -q "Sync completed with failures" "$out"; then crash=$(grep -oE '^[A-Za-z_][A-Za-z0-9_.]*(Error|Exception)\b' "$out" | tail -n 1) if [ -n "$crash" ]; then reason="crashed ($crash)" else reason="exited $status before reporting a result" fi push_failure "$provider: $reason journalctl --user -u ${AICHAT_SYNC_UNIT:-aichat-sync} -n 100" fi fi rm -f "$out" done exit "$rc"