|
| 1 | +#!/usr/bin/env bash |
| 2 | +# dolt-archive/run.sh — Deterministic JSONL backup + git push + dolt push. |
| 3 | +# |
| 4 | +# Exports production databases to JSONL, commits to git backup repo, |
| 5 | +# and pushes Dolt remotes. JSONL is the last-resort recovery layer. |
| 6 | +# |
| 7 | +# Usage: ./run.sh [--databases db1,db2,...] [--skip-git] [--skip-dolt-push] |
| 8 | + |
| 9 | +set -euo pipefail |
| 10 | + |
| 11 | +# --- Configuration ----------------------------------------------------------- |
| 12 | + |
| 13 | +DOLT_HOST="${DOLT_HOST:-127.0.0.1}" |
| 14 | +DOLT_PORT="${DOLT_PORT:-3307}" |
| 15 | +DOLT_USER="${DOLT_USER:-root}" |
| 16 | +DOLT_DATA_DIR="${DOLT_DATA_DIR:-$HOME/gt/.dolt-data}" |
| 17 | +JSONL_EXPORT_DIR="$HOME/gt/.dolt-archive/jsonl" |
| 18 | +BACKUP_REPO="$HOME/gt/.dolt-archive/git" |
| 19 | +DEFAULT_DBS="hq,bd,gastown" |
| 20 | +SKIP_GIT=false |
| 21 | +SKIP_DOLT_PUSH=false |
| 22 | + |
| 23 | +# --- Argument parsing -------------------------------------------------------- |
| 24 | + |
| 25 | +while [[ $# -gt 0 ]]; do |
| 26 | + case "$1" in |
| 27 | + --databases) DEFAULT_DBS="$2"; shift 2 ;; |
| 28 | + --skip-git) SKIP_GIT=true; shift ;; |
| 29 | + --skip-dolt-push) SKIP_DOLT_PUSH=true; shift ;; |
| 30 | + --help|-h) |
| 31 | + echo "Usage: $0 [--databases db1,db2,...] [--skip-git] [--skip-dolt-push]" |
| 32 | + exit 0 |
| 33 | + ;; |
| 34 | + *) echo "Unknown option: $1"; exit 1 ;; |
| 35 | + esac |
| 36 | +done |
| 37 | + |
| 38 | +# --- Helpers ----------------------------------------------------------------- |
| 39 | + |
| 40 | +log() { |
| 41 | + echo "[dolt-archive] $*" |
| 42 | +} |
| 43 | + |
| 44 | +LOGFILE=$(mktemp /tmp/dolt-archive-stderr.XXXXXX) |
| 45 | +trap 'rm -f "$LOGFILE"' EXIT |
| 46 | + |
| 47 | +dolt_query() { |
| 48 | + local db="$1" |
| 49 | + local query="$2" |
| 50 | + local args=(dolt --host "$DOLT_HOST" --port "$DOLT_PORT" --no-tls -u "$DOLT_USER" -p "") |
| 51 | + if [[ -n "$db" ]]; then |
| 52 | + args+=(--use-db "$db") |
| 53 | + fi |
| 54 | + args+=(sql -q "$query" --result-format csv) |
| 55 | + "${args[@]}" 2>>"$LOGFILE" | tail -n +2 | tr -d '\r' |
| 56 | +} |
| 57 | + |
| 58 | +dolt_query_json() { |
| 59 | + local db="$1" |
| 60 | + local query="$2" |
| 61 | + dolt --host "$DOLT_HOST" --port "$DOLT_PORT" --no-tls -u "$DOLT_USER" -p "" \ |
| 62 | + --use-db "$db" sql -q "$query" --result-format json 2>>"$LOGFILE" |
| 63 | +} |
| 64 | + |
| 65 | +# --- Step 1: JSONL export ---------------------------------------------------- |
| 66 | + |
| 67 | +IFS=',' read -ra PROD_DBS <<< "$DEFAULT_DBS" |
| 68 | + |
| 69 | +log "Starting archive cycle (databases: ${PROD_DBS[*]})" |
| 70 | +mkdir -p "$JSONL_EXPORT_DIR" |
| 71 | + |
| 72 | +EXPORTED=0 |
| 73 | +EXPORT_FAILED=0 |
| 74 | +EXPORT_ERRORS="" |
| 75 | + |
| 76 | +for DB in "${PROD_DBS[@]}"; do |
| 77 | + EXPORT_FILE="$JSONL_EXPORT_DIR/${DB}-$(date +%Y%m%d-%H%M).jsonl" |
| 78 | + LATEST_LINK="$JSONL_EXPORT_DIR/${DB}-latest.jsonl" |
| 79 | + |
| 80 | + log "Exporting $DB..." |
| 81 | + |
| 82 | + # Try bd export first (native beads export) |
| 83 | + if bd export --db "$DB" --format jsonl > "$EXPORT_FILE" 2>/dev/null; then |
| 84 | + LINE_COUNT=$(wc -l < "$EXPORT_FILE" | tr -d ' ') |
| 85 | + FILE_SIZE=$(du -h "$EXPORT_FILE" | cut -f1) |
| 86 | + log " $DB: $LINE_COUNT issues exported ($FILE_SIZE) [bd export]" |
| 87 | + ln -sf "$(basename "$EXPORT_FILE")" "$LATEST_LINK" |
| 88 | + EXPORTED=$((EXPORTED + 1)) |
| 89 | + else |
| 90 | + # Fallback: query Dolt directly for issues table |
| 91 | + if dolt_query_json "$DB" "SELECT * FROM issues ORDER BY id" > "$EXPORT_FILE" 2>/dev/null && [[ -s "$EXPORT_FILE" ]]; then |
| 92 | + LINE_COUNT=$(wc -l < "$EXPORT_FILE" | tr -d ' ') |
| 93 | + log " $DB: exported via SQL ($LINE_COUNT lines)" |
| 94 | + ln -sf "$(basename "$EXPORT_FILE")" "$LATEST_LINK" |
| 95 | + EXPORTED=$((EXPORTED + 1)) |
| 96 | + else |
| 97 | + log " WARN: $DB export failed" |
| 98 | + rm -f "$EXPORT_FILE" |
| 99 | + EXPORT_FAILED=$((EXPORT_FAILED + 1)) |
| 100 | + EXPORT_ERRORS="${EXPORT_ERRORS}${DB} " |
| 101 | + fi |
| 102 | + fi |
| 103 | +done |
| 104 | + |
| 105 | +# Prune old exports (keep last 24 snapshots per DB) |
| 106 | +for DB in "${PROD_DBS[@]}"; do |
| 107 | + SNAPSHOTS=$(ls -t "$JSONL_EXPORT_DIR/${DB}-2"*.jsonl 2>/dev/null | tail -n +25) |
| 108 | + if [[ -n "$SNAPSHOTS" ]]; then |
| 109 | + echo "$SNAPSHOTS" | xargs rm -f |
| 110 | + log "Pruned old $DB snapshots" |
| 111 | + fi |
| 112 | +done |
| 113 | + |
| 114 | +log "JSONL export: $EXPORTED succeeded, $EXPORT_FAILED failed" |
| 115 | + |
| 116 | +# --- Step 2: Git commit and push --------------------------------------------- |
| 117 | + |
| 118 | +GIT_PUSHED=false |
| 119 | + |
| 120 | +if ! $SKIP_GIT && [[ -d "$BACKUP_REPO/.git" ]]; then |
| 121 | + log "" |
| 122 | + log "=== Git Push ===" |
| 123 | + |
| 124 | + # Copy latest JSONL files to git repo |
| 125 | + for DB in "${PROD_DBS[@]}"; do |
| 126 | + LATEST="$JSONL_EXPORT_DIR/${DB}-latest.jsonl" |
| 127 | + if [[ -L "$LATEST" ]]; then |
| 128 | + REAL_FILE="$JSONL_EXPORT_DIR/$(readlink "$LATEST")" |
| 129 | + if [[ -f "$REAL_FILE" ]]; then |
| 130 | + cp "$REAL_FILE" "$BACKUP_REPO/${DB}.jsonl" |
| 131 | + fi |
| 132 | + elif [[ -f "$LATEST" ]]; then |
| 133 | + cp "$LATEST" "$BACKUP_REPO/${DB}.jsonl" |
| 134 | + fi |
| 135 | + done |
| 136 | + |
| 137 | + cd "$BACKUP_REPO" |
| 138 | + |
| 139 | + if git diff --quiet && git diff --staged --quiet; then |
| 140 | + log "No changes to commit" |
| 141 | + else |
| 142 | + git add *.jsonl 2>/dev/null || true |
| 143 | + git commit -m "Archive snapshot $(date +%Y-%m-%d-%H%M)" \ |
| 144 | + --author="Gas Town Archive <archive@gastown.local>" 2>/dev/null || true |
| 145 | + |
| 146 | + if git remote get-url origin > /dev/null 2>&1; then |
| 147 | + if git push origin main 2>/dev/null; then |
| 148 | + GIT_PUSHED=true |
| 149 | + log "Pushed to GitHub" |
| 150 | + else |
| 151 | + log "WARN: Git push to remote failed" |
| 152 | + fi |
| 153 | + else |
| 154 | + log "WARN: No git remote configured for backup repo" |
| 155 | + fi |
| 156 | + fi |
| 157 | +elif ! $SKIP_GIT; then |
| 158 | + log "No git backup repo at $BACKUP_REPO — skipping git push" |
| 159 | +fi |
| 160 | + |
| 161 | +# --- Step 3: Dolt native push ------------------------------------------------ |
| 162 | + |
| 163 | +DOLT_PUSHED=0 |
| 164 | +DOLT_PUSH_FAILED=0 |
| 165 | + |
| 166 | +if ! $SKIP_DOLT_PUSH; then |
| 167 | + log "" |
| 168 | + log "=== Dolt Push ===" |
| 169 | + |
| 170 | + for DB in "${PROD_DBS[@]}"; do |
| 171 | + DB_DIR="$DOLT_DATA_DIR/$DB" |
| 172 | + |
| 173 | + if [[ ! -d "$DB_DIR/.dolt" ]]; then |
| 174 | + log " $DB: no .dolt directory, skipping" |
| 175 | + continue |
| 176 | + fi |
| 177 | + |
| 178 | + REMOTES=$(cd "$DB_DIR" && dolt remote -v 2>/dev/null | grep -v "^$" | head -5) |
| 179 | + if [[ -z "$REMOTES" ]]; then |
| 180 | + log " $DB: no remotes configured, skipping" |
| 181 | + continue |
| 182 | + fi |
| 183 | + |
| 184 | + log " $DB: pushing to remotes..." |
| 185 | + cd "$DB_DIR" |
| 186 | + |
| 187 | + for REMOTE_NAME in $(dolt remote -v 2>/dev/null | awk '{print $1}' | sort -u); do |
| 188 | + if timeout 120 dolt push "$REMOTE_NAME" main 2>/dev/null; then |
| 189 | + log " $REMOTE_NAME: pushed" |
| 190 | + DOLT_PUSHED=$((DOLT_PUSHED + 1)) |
| 191 | + else |
| 192 | + log " $REMOTE_NAME: FAILED" |
| 193 | + DOLT_PUSH_FAILED=$((DOLT_PUSH_FAILED + 1)) |
| 194 | + fi |
| 195 | + done |
| 196 | + done |
| 197 | + |
| 198 | + log "Dolt push: $DOLT_PUSHED succeeded, $DOLT_PUSH_FAILED failed" |
| 199 | +fi |
| 200 | + |
| 201 | +# --- Step 4: Report results -------------------------------------------------- |
| 202 | + |
| 203 | +log "" |
| 204 | +log "=== Archive Cycle Complete ===" |
| 205 | + |
| 206 | +SUMMARY="Archive: jsonl=$EXPORTED/$((EXPORTED + EXPORT_FAILED)), git=${GIT_PUSHED}, dolt_push=$DOLT_PUSHED/$((DOLT_PUSHED + DOLT_PUSH_FAILED))" |
| 207 | +log "$SUMMARY" |
| 208 | + |
| 209 | +RESULT="success" |
| 210 | +if [[ "$EXPORT_FAILED" -gt 0 ]] || [[ "$DOLT_PUSH_FAILED" -gt 0 ]]; then |
| 211 | + RESULT="warning" |
| 212 | +fi |
| 213 | + |
| 214 | +bd create "$SUMMARY" -t chore --ephemeral \ |
| 215 | + -l type:plugin-run,plugin:dolt-archive,result:$RESULT \ |
| 216 | + -d "$SUMMARY" --silent 2>/dev/null || true |
| 217 | + |
| 218 | +if [[ "$EXPORT_FAILED" -gt 0 ]]; then |
| 219 | + gt escalate "dolt-archive: JSONL export failed for $EXPORT_FAILED databases ($EXPORT_ERRORS)" \ |
| 220 | + -s critical \ |
| 221 | + --reason "JSONL is our last-resort recovery layer. Failed databases: $EXPORT_ERRORS" 2>/dev/null || true |
| 222 | +fi |
| 223 | + |
| 224 | +log "Done." |
0 commit comments