/ dev / publish_osf_wiki.sh-2
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