publish_osf_wiki.sh-2
1 #!/usr/bin/env bash 2 set -uo pipefail 3 4 # === Constants and Paths === 5 BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 6 OSF_YAML="$BASEDIR/osf.yaml" 7 GITFIELD_DIR="$BASEDIR/.gitfield" 8 LOG_DIR="$GITFIELD_DIR/logs" 9 SCAN_LOG_PUSH="$GITFIELD_DIR/push_log.json" 10 TMP_JSON_TOKEN="$GITFIELD_DIR/tmp_token.json" 11 TMP_JSON_PROJECT="$GITFIELD_DIR/tmp_project.json" 12 TMP_JSON_WIKI="$GITFIELD_DIR/tmp_wiki.json" 13 TOKEN_PATH="$HOME/.local/gitfieldlib/osf.token" 14 mkdir -p "$GITFIELD_DIR" "$LOG_DIR" "$(dirname "$TOKEN_PATH")" 15 chmod -R u+rw "$GITFIELD_DIR" "$(dirname "$TOKEN_PATH")" 16 17 # === Logging === 18 log() { 19 local level="$1" msg="$2" 20 echo "[$(date -Iseconds)] [$level] $msg" >> "$LOG_DIR/gitfield_wiki_$(date +%Y%m%d).log" 21 if [[ "$level" == "ERROR" || "$level" == "INFO" || "$VERBOSE" == "true" ]]; then 22 echo "[$(date -Iseconds)] [$level] $msg" >&2 23 fi 24 } 25 26 error() { 27 log "ERROR" "$1" 28 exit 1 29 } 30 31 # === Dependency Check === 32 require_yq() { 33 if ! command -v yq &>/dev/null || ! yq --version 2>/dev/null | grep -q 'version v4'; then 34 log "INFO" "Installing 'yq' (Go version)..." 35 YQ_BIN="/usr/local/bin/yq" 36 ARCH=$(uname -m) 37 case $ARCH in 38 x86_64) ARCH=amd64 ;; 39 aarch64) ARCH=arm64 ;; 40 *) error "Unsupported architecture: $ARCH" ;; 41 esac 42 curl -sL "https://github.com/mikefarah/yq/releases/download/v4.43.1/yq_linux_${ARCH}" -o yq \ 43 && chmod +x yq && sudo mv yq "$YQ_BIN" 44 log "INFO" "'yq' installed to $YQ_BIN" 45 fi 46 } 47 48 require_jq() { 49 if ! command -v jq &>/dev/null; then 50 log "INFO" "Installing 'jq'..." 51 sudo apt update && sudo apt install -y jq 52 log "INFO" "'jq' installed" 53 fi 54 } 55 56 require_curl() { 57 if ! command -v curl &>/dev/null; then 58 log "INFO" "Installing 'curl'..." 59 sudo apt update && sudo apt install -y curl 60 log "INFO" "'curl' installed" 61 fi 62 CURL_VERSION=$(curl --version | head -n 1) 63 log "INFO" "Using curl version: $CURL_VERSION" 64 } 65 66 require_yq 67 require_jq 68 require_curl 69 70 # === Token Retrieval === 71 get_token() { 72 if [[ -z "${OSF_TOKEN:-}" ]]; then 73 if [[ -f "$TOKEN_PATH" ]]; then 74 OSF_TOKEN=$(tr -d '\n' < "$TOKEN_PATH") 75 if [[ -z "$OSF_TOKEN" ]]; then 76 log "ERROR" "OSF token file $TOKEN_PATH is empty" 77 echo -n "🔐 Enter your OSF_TOKEN: " >&2 78 read -rs OSF_TOKEN 79 echo >&2 80 echo "$OSF_TOKEN" > "$TOKEN_PATH" 81 chmod 600 "$TOKEN_PATH" 82 log "INFO" "Token saved to $TOKEN_PATH" 83 fi 84 else 85 echo -n "🔐 Enter your OSF_TOKEN: " >&2 86 read -rs OSF_TOKEN 87 echo >&2 88 echo "$OSF_TOKEN" > "$TOKEN_PATH" 89 chmod 600 "$TOKEN_PATH" 90 log "INFO" "Token saved to $TOKEN_PATH" 91 fi 92 fi 93 log "DEBUG" "OSF_TOKEN length: ${#OSF_TOKEN}" 94 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_TOKEN" "https://api.osf.io/v2/users/me/" \ 95 -H "Authorization: Bearer $OSF_TOKEN" 2>> "$LOG_DIR/curl_errors.log") 96 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 97 if [[ -z "$HTTP_CODE" ]]; then 98 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 99 error "Failed to validate OSF token: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 100 fi 101 if [[ "$HTTP_CODE" != "200" ]]; then 102 RESPONSE_BODY=$(cat "$TMP_JSON_TOKEN") 103 error "Invalid OSF token (HTTP $HTTP_CODE): $RESPONSE_BODY" 104 fi 105 } 106 107 # === Validate YAML === 108 validate_yaml() { 109 log "INFO" "Validating $OSF_YAML..." 110 [[ -f "$OSF_YAML" ]] || error "No osf.yaml found. Run publish_osf.sh --init first." 111 for field in title description category public; do 112 [[ $(yq e ".$field" "$OSF_YAML") != "null" ]] || error "Missing field: $field in $OSF_YAML" 113 done 114 } 115 116 # === Read Project ID === 117 read_project_id() { 118 if [[ ! -f "$SCAN_LOG_PUSH" ]] || ! jq -e '.' "$SCAN_LOG_PUSH" >/dev/null 2>&1; then 119 log "WARN" "No valid push_log.json found" 120 echo "" 121 return 122 fi 123 NODE_ID=$(jq -r '.project_id // ""' "$SCAN_LOG_PUSH") 124 echo "$NODE_ID" 125 } 126 127 # === Search for Existing Project by Title === 128 find_project_by_title() { 129 local title="$1" 130 log "INFO" "Searching for project: $title" 131 if [[ "$DRY_RUN" == "true" ]]; then 132 echo "dry-run-$(uuidgen)" 133 return 134 fi 135 ENCODED_TITLE=$(jq -r -n --arg title "$title" '$title|@uri') 136 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_PROJECT" "https://api.osf.io/v2/nodes/?filter[title]=$ENCODED_TITLE" \ 137 -H "Authorization: Bearer $OSF_TOKEN" 2>> "$LOG_DIR/curl_errors.log") 138 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 139 if [[ -z "$HTTP_CODE" ]]; then 140 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 141 error "Failed to search for project: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 142 fi 143 if [[ "$HTTP_CODE" != "200" ]]; then 144 RESPONSE_BODY=$(cat "$TMP_JSON_PROJECT") 145 log "WARN" "Failed to search for project (HTTP $HTTP_CODE): $RESPONSE_BODY" 146 echo "" 147 return 148 fi 149 NODE_ID=$(jq -r '.data[0].id // ""' "$TMP_JSON_PROJECT") 150 [[ -n "$NODE_ID" ]] && log "INFO" "Found project '$title': $NODE_ID" 151 echo "$NODE_ID" 152 } 153 154 # === Check and Enable Wiki Settings === 155 check_wiki_settings() { 156 log "INFO" "Checking wiki settings for project $NODE_ID..." 157 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_PROJECT" "https://api.osf.io/v2/nodes/$NODE_ID/" \ 158 -H "Authorization: Bearer $OSF_TOKEN" 2>> "$LOG_DIR/curl_errors.log") 159 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 160 if [[ -z "$HTTP_CODE" ]]; then 161 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 162 error "Failed to fetch project settings: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 163 fi 164 if [[ "$HTTP_CODE" != "200" ]]; then 165 RESPONSE_BODY=$(cat "$TMP_JSON_PROJECT") 166 error "Failed to fetch project settings (HTTP $HTTP_CODE): $RESPONSE_BODY" 167 fi 168 WIKI_ENABLED=$(jq -r '.data.attributes.wiki_enabled // false' "$TMP_JSON_PROJECT") 169 if [[ "$WIKI_ENABLED" != "true" ]]; then 170 log "INFO" "Wiki is disabled. Attempting to enable..." 171 RESPONSE=$(curl -s -w "\n%{http_code}" -X PATCH "https://api.osf.io/v2/nodes/$NODE_ID/" \ 172 -H "Authorization: Bearer $OSF_TOKEN" \ 173 -H "Content-Type: application/vnd.api+json" \ 174 -d @- <<EOF 175 { 176 "data": { 177 "id": "$NODE_ID", 178 "type": "nodes", 179 "attributes": { 180 "wiki_enabled": true 181 } 182 } 183 } 184 EOF 185 2>> "$LOG_DIR/curl_errors.log") 186 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 187 if [[ -z "$HTTP_CODE" ]]; then 188 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 189 error "Failed to enable wiki: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 190 fi 191 if [[ "$HTTP_CODE" != "200" ]]; then 192 RESPONSE_BODY=$(cat "$TMP_JSON_PROJECT") 193 error "Failed to enable wiki for project $NODE_ID (HTTP $HTTP_CODE): $RESPONSE_BODY" 194 fi 195 log "INFO" "Wiki enabled successfully" 196 fi 197 } 198 199 # === Check for Existing Wiki Page === 200 check_wiki_exists() { 201 local retries=3 202 local attempt=1 203 while [[ $attempt -le $retries ]]; do 204 log "INFO" "Checking for existing wiki page (attempt $attempt/$retries)..." 205 # URL-encode the filter parameter to avoid shell interpretation 206 FILTER_ENCODED=$(jq -r -n --arg filter "home" '$filter|@uri') 207 WIKI_URL="https://api.osf.io/v2/nodes/$NODE_ID/wikis/?filter[name]=$FILTER_ENCODED" 208 log "DEBUG" "Executing curl: curl -s -w '\n%{http_code}' -o '$TMP_JSON_WIKI' '$WIKI_URL' -H 'Authorization: Bearer [REDACTED]'" 209 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_WIKI" "$WIKI_URL" \ 210 -H "Authorization: Bearer $OSF_TOKEN" 2>> "$LOG_DIR/curl_errors.log") 211 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 212 if [[ -z "$HTTP_CODE" ]]; then 213 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 214 if [[ $attempt -eq $retries ]]; then 215 error "Failed to check for wiki page: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 216 fi 217 log "WARN" "curl command failed (no HTTP code returned). Retrying in 5 seconds..." 218 sleep 5 219 ((attempt++)) 220 continue 221 fi 222 if [[ "$HTTP_CODE" != "200" ]]; then 223 RESPONSE_BODY="No response body" 224 [[ -f "$TMP_JSON_WIKI" ]] && RESPONSE_BODY=$(cat "$TMP_JSON_WIKI") 225 error "Failed to check for wiki page (HTTP $HTTP_CODE): $RESPONSE_BODY" 226 fi 227 WIKI_ID=$(jq -r '.data[0].id // ""' "$TMP_JSON_WIKI") 228 if [[ -n "$WIKI_ID" ]]; then 229 log "INFO" "Found existing wiki page 'home' (ID: $WIKI_ID)" 230 return 0 231 else 232 log "INFO" "No 'home' wiki page found" 233 return 1 234 fi 235 done 236 } 237 238 # === Create Wiki Page === 239 create_wiki_page() { 240 local wiki_path="$1" 241 log "INFO" "Creating new wiki page 'home'..." 242 CONTENT=$(jq -Rs . < "$wiki_path") 243 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_WIKI" -X POST "https://api.osf.io/v2/nodes/$NODE_ID/wikis/" \ 244 -H "Authorization: Bearer $OSF_TOKEN" \ 245 -H "Content-Type: application/vnd.api+json" \ 246 -d @- <<EOF 247 { 248 "data": { 249 "type": "wikis", 250 "attributes": { 251 "name": "home", 252 "content": $CONTENT 253 } 254 } 255 } 256 EOF 257 2>> "$LOG_DIR/curl_errors.log") 258 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 259 if [[ -z "$HTTP_CODE" ]]; then 260 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 261 error "Failed to create wiki page: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 262 fi 263 if [[ "$HTTP_CODE" != "201" ]]; then 264 RESPONSE_BODY="No response body" 265 [[ -f "$TMP_JSON_WIKI" ]] && RESPONSE_BODY=$(cat "$TMP_JSON_WIKI") 266 error "Failed to create wiki page (HTTP $HTTP_CODE): $RESPONSE_BODY" 267 fi 268 log "INFO" "Wiki page 'home' created successfully" 269 } 270 271 # === Generate Default Wiki with Links === 272 generate_wiki() { 273 local wiki_path="$1" 274 log "INFO" "Generating default wiki at $wiki_path..." 275 mkdir -p "$(dirname "$wiki_path")" 276 { 277 echo "# Auto-Generated Wiki for $(yq e '.title' "$OSF_YAML")" 278 echo 279 echo "## Project Overview" 280 echo "$(yq e '.description' "$OSF_YAML")" 281 echo 282 echo "## Repository Info" 283 echo "- **Last Commit**: $(git log -1 --pretty=%B 2>/dev/null || echo "No git commits")" 284 echo "- **Commit Hash**: $(git rev-parse HEAD 2>/dev/null || echo "N/A")" 285 if [[ -f "$(yq e '.readme.path' "$OSF_YAML")" ]]; then 286 echo 287 echo "## README Preview" 288 head -n 10 "$(yq e '.readme.path' "$OSF_YAML")" 289 fi 290 echo 291 echo "## Internal Documents" 292 echo "Links to documents uploaded to OSF:" 293 for section in docs essays images scripts data files; do 294 local count 295 count=$(yq e ".${section} | length" "$OSF_YAML") 296 if [[ "$count" != "0" && "$count" != "null" ]]; then 297 echo 298 echo "### $(echo "$section" | tr '[:lower:]' '[:upper:]')" 299 for ((i = 0; i < count; i++)); do 300 local name 301 name=$(yq e ".${section}[$i].name" "$OSF_YAML") 302 echo "- [$name](https://osf.io/$NODE_ID/files/osfstorage/$name)" 303 done 304 fi 305 done 306 } > "$wiki_path" 307 log "INFO" "Default wiki generated at $wiki_path" 308 } 309 310 # === Push Wiki to OSF === 311 push_wiki() { 312 local wiki_path="$1" 313 log "INFO" "Pushing wiki from $wiki_path" 314 if [[ "$DRY_RUN" == "true" ]]; then 315 log "DRY-RUN" "Would push wiki to $NODE_ID" 316 return 0 317 fi 318 319 # Check if wiki exists; create if it doesn't 320 if ! check_wiki_exists; then 321 create_wiki_page "$wiki_path" 322 return 0 # Creation includes content, so no need to patch 323 fi 324 325 # Wiki exists, update it with PATCH 326 CONTENT=$(jq -Rs . < "$wiki_path") 327 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_WIKI" -X PATCH "https://api.osf.io/v2/nodes/$NODE_ID/wikis/home/" \ 328 -H "Authorization: Bearer $OSF_TOKEN" \ 329 -H "Content-Type: application/vnd.api+json" \ 330 -d @- <<EOF 331 { 332 "data": { 333 "type": "wikis", 334 "attributes": { 335 "content": $CONTENT 336 } 337 } 338 } 339 EOF 340 2>> "$LOG_DIR/curl_errors.log") 341 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 342 if [[ -z "$HTTP_CODE" ]]; then 343 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 344 log "ERROR" "Failed to push wiki: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 345 return 1 346 fi 347 if [[ "$HTTP_CODE" != "200" ]]; then 348 RESPONSE_BODY="No response body" 349 [[ -f "$TMP_JSON_WIKI" ]] && RESPONSE_BODY=$(cat "$TMP_JSON_WIKI") 350 log "ERROR" "Failed to push wiki (HTTP $HTTP_CODE): $RESPONSE_BODY" 351 return 1 352 fi 353 echo "📜 Pushed wiki to https://osf.io/$NODE_ID/" >&2 354 return 0 355 } 356 357 # === Main Logic === 358 wiki_mode() { 359 validate_yaml 360 get_token 361 362 local title 363 title=$(yq e '.title' "$OSF_YAML") 364 365 NODE_ID=$(read_project_id) 366 if [[ -n "$NODE_ID" ]]; then 367 log "INFO" "Using existing OSF project ID: $NODE_ID" 368 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_PROJECT" "https://api.osf.io/v2/nodes/$NODE_ID/" \ 369 -H "Authorization: Bearer $OSF_TOKEN" 2>> "$LOG_DIR/curl_errors.log") 370 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 371 if [[ -z "$HTTP_CODE" ]]; then 372 CURL_ERROR=$(cat "$LOG_DIR/curl_errors.log") 373 error "Failed to validate project ID: curl command failed (no HTTP code returned). Curl error: $CURL_ERROR" 374 fi 375 if [[ "$HTTP_CODE" != "200" ]]; then 376 log "WARN" "Project $NODE_ID not found (HTTP $HTTP_CODE)" 377 NODE_ID="" 378 fi 379 fi 380 381 if [[ -z "$NODE_ID" ]]; then 382 NODE_ID=$(find_project_by_title "$title") 383 fi 384 385 [[ -n "$NODE_ID" ]] || error "Failed to determine OSF project ID" 386 387 # Check and enable wiki settings 388 check_wiki_settings 389 390 local wiki_path 391 wiki_path=$(yq e '.wiki.path' "$OSF_YAML") 392 if [[ "$wiki_path" == "null" || -z "$wiki_path" ]]; then 393 log "INFO" "No wiki defined in osf.yaml. Auto-generating..." 394 wiki_path="docs/generated_wiki.md" 395 echo "wiki:" >> "$OSF_YAML" 396 echo " path: \"$wiki_path\"" >> "$OSF_YAML" 397 echo " overwrite: true" >> "$OSF_YAML" 398 fi 399 400 wiki_path="$BASEDIR/$wiki_path" 401 if [[ ! -f "$wiki_path" ]]; then 402 generate_wiki "$wiki_path" 403 fi 404 405 push_wiki "$wiki_path" || error "Wiki push failed" 406 log "INFO" "Wiki push complete for project $NODE_ID" 407 echo "✅ Wiki push complete! View at: https://osf.io/$NODE_ID/wiki/" >&2 408 } 409 410 # === Help Menu === 411 show_help() { 412 local verbose="$1" 413 if [[ "$verbose" == "true" ]]; then 414 cat <<EOF 415 Usage: $0 [OPTION] 416 417 Publish a wiki page to an OSF project. 418 419 Options: 420 --push Generate (if needed) and push wiki to OSF 421 --dry-run Simulate actions without making API calls 422 --verbose Show detailed logs on stderr 423 --help Show this help message (--help --verbose for more details) 424 425 Behavior: 426 - Requires osf.yaml (run publish_osf.sh --init first if missing). 427 - Auto-generates a wiki (docs/generated_wiki.md) if none is defined in osf.yaml. 428 - Updates osf.yaml with the new wiki path if auto-generated. 429 - Pushes the wiki to the OSF project's wiki/home endpoint. 430 - Includes links to internal documents (docs, scripts, etc.) from osf.yaml. 431 432 Example: 433 $0 --push # Push wiki to OSF 434 $0 --dry-run --push # Simulate push 435 EOF 436 else 437 cat <<EOF 438 Usage: $0 [OPTION] 439 440 Publish a wiki page to an OSF project. 441 442 Options: 443 --push Push wiki to OSF 444 --dry-run Simulate actions 445 --verbose Show detailed logs 446 --help Show this help (--help --verbose for more) 447 448 Example: 449 $0 --push # Push wiki to OSF 450 EOF 451 fi 452 } 453 454 # === CLI Dispatcher === 455 DRY_RUN="false" 456 VERBOSE="false" 457 MODE="" 458 while [[ $# -gt 0 ]]; do 459 case "$1" in 460 --push) MODE="wiki" ;; 461 --dry-run) DRY_RUN="true" ;; 462 --verbose) VERBOSE="true" ;; 463 --help) show_help "$VERBOSE"; exit 0 ;; 464 *) echo "Unknown option: $1" >&2; show_help "false"; exit 1 ;; 465 esac 466 shift 467 done 468 469 case "$MODE" in 470 wiki) wiki_mode ;; 471 *) show_help "false"; exit 0 ;; 472 esac