# GitLab API Fallback Utility

## Direct GitLab API Integration for Reliable CI/CD Operations

This utility provides direct GitLab API methods as a fallback when `glab` commands fail. Based on production feedback, many glab commands fail with permission errors even with valid authentication. This utility ensures reliable GitLab operations through direct API calls.

## API Setup & Configuration

### Initialize GitLab API Environment

```bash
# Initialize GitLab API configuration
gitlab_api_init() {
  # Set default GitLab host if not provided
  export GITLAB_HOST="${GITLAB_HOST:-https://gitlab.com}"

  # Try to get token from glab first, then environment
  if [[ -z "$GITLAB_TOKEN" ]]; then
    export GITLAB_TOKEN="$(glab auth token 2>/dev/null || echo "")"
  fi

  # Get project ID from glab or environment
  if [[ -z "$CI_PROJECT_ID" ]]; then
    export CI_PROJECT_ID="$(glab repo view --output json 2>/dev/null | jq -r '.id // ""' || echo "")"
  fi

  # Validate required variables
  if [[ -z "$GITLAB_TOKEN" ]]; then
    echo "❌ GitLab token not found. Please set GITLAB_TOKEN environment variable."
    return 1
  fi

  if [[ -z "$CI_PROJECT_ID" ]]; then
    echo "❌ Project ID not found. Please set CI_PROJECT_ID or run from a GitLab repository."
    return 1
  fi

  # URL encode project ID if needed (for projects with namespaces)
  export CI_PROJECT_ID_ENCODED=$(echo "$CI_PROJECT_ID" | sed 's/\//%2F/g')

  echo "✅ GitLab API initialized:"
  echo "   Host: $GITLAB_HOST"
  echo "   Project: $CI_PROJECT_ID"
  echo "   Token: ${GITLAB_TOKEN:0:8}..."

  return 0
}

# Generic API request function with error handling
gitlab_api_request() {
  local method="${1:-GET}"
  local endpoint="$2"
  local data="$3"

  gitlab_api_init || return 1

  local url="$GITLAB_HOST/api/v4/$endpoint"
  local curl_opts=(-s -H "Authorization: Bearer $GITLAB_TOKEN" -H "Content-Type: application/json")

  case "$method" in
    GET)
      curl "${curl_opts[@]}" "$url"
      ;;
    POST)
      curl "${curl_opts[@]}" -X POST ${data:+-d "$data"} "$url"
      ;;
    PUT)
      curl "${curl_opts[@]}" -X PUT ${data:+-d "$data"} "$url"
      ;;
    DELETE)
      curl "${curl_opts[@]}" -X DELETE "$url"
      ;;
    *)
      echo "❌ Unsupported method: $method"
      return 1
      ;;
  esac
}
```

## Pipeline Operations

### Get Pipeline Information

```bash
# Get pipeline details by ID
gitlab_get_pipeline() {
  local pipeline_id="$1"

  if [[ -z "$pipeline_id" ]]; then
    echo "❌ Pipeline ID required"
    return 1
  fi

  gitlab_api_request GET "projects/$CI_PROJECT_ID_ENCODED/pipelines/$pipeline_id"
}

# Get latest pipeline for branch
gitlab_get_latest_pipeline() {
  local branch="${1:-$(git branch --show-current)}"

  local response=$(gitlab_api_request GET "projects/$CI_PROJECT_ID_ENCODED/pipelines?ref=$branch&per_page=1")
  echo "$response" | jq '.[0] // empty'
}

# List pipelines with pagination
gitlab_list_pipelines() {
  local branch="${1:-}"
  local limit="${2:-20}"

  local query="per_page=$limit"
  [[ -n "$branch" ]] && query="$query&ref=$branch"

  gitlab_api_request GET "projects/$CI_PROJECT_ID_ENCODED/pipelines?$query"
}

# Get pipeline status with detailed job information
gitlab_get_pipeline_status() {
  local pipeline_id="$1"

  if [[ -z "$pipeline_id" ]]; then
    # Get latest pipeline if no ID provided
    pipeline_id=$(gitlab_get_latest_pipeline | jq -r '.id // empty')
    if [[ -z "$pipeline_id" ]]; then
      echo "❌ No pipeline found"
      return 1
    fi
  fi

  local pipeline_info=$(gitlab_get_pipeline "$pipeline_id")
  local jobs_info=$(gitlab_get_pipeline_jobs "$pipeline_id")

  # Combine pipeline and job information
  echo "$pipeline_info" | jq --argjson jobs "$jobs_info" '. + {jobs: $jobs}'
}
```

### Pipeline Control Operations

```bash
# Retry pipeline
gitlab_retry_pipeline() {
  local pipeline_id="$1"

  if [[ -z "$pipeline_id" ]]; then
    echo "❌ Pipeline ID required"
    return 1
  fi

  gitlab_api_request POST "projects/$CI_PROJECT_ID_ENCODED/pipelines/$pipeline_id/retry"
}

# Cancel pipeline
gitlab_cancel_pipeline() {
  local pipeline_id="$1"

  if [[ -z "$pipeline_id" ]]; then
    echo "❌ Pipeline ID required"
    return 1
  fi

  gitlab_api_request POST "projects/$CI_PROJECT_ID_ENCODED/pipelines/$pipeline_id/cancel"
}

# Trigger new pipeline
gitlab_trigger_pipeline() {
  local branch="${1:-$(git branch --show-current)}"
  local variables="${2:-}"

  local data="{\"ref\": \"$branch\""
  if [[ -n "$variables" ]]; then
    data="$data, \"variables\": $variables"
  fi
  data="$data}"

  gitlab_api_request POST "projects/$CI_PROJECT_ID_ENCODED/pipeline" "$data"
}
```

## Job Operations

### Get Job Information

```bash
# Get all jobs for a pipeline
gitlab_get_pipeline_jobs() {
  local pipeline_id="$1"

  if [[ -z "$pipeline_id" ]]; then
    echo "❌ Pipeline ID required"
    return 1
  fi

  gitlab_api_request GET "projects/$CI_PROJECT_ID_ENCODED/pipelines/$pipeline_id/jobs?per_page=100"
}

# Get job by name in pipeline
gitlab_get_job_by_name() {
  local pipeline_id="$1"
  local job_name="$2"

  if [[ -z "$pipeline_id" ]] || [[ -z "$job_name" ]]; then
    echo "❌ Pipeline ID and job name required"
    return 1
  fi

  gitlab_get_pipeline_jobs "$pipeline_id" | jq -r ".[] | select(.name == \"$job_name\")"
}

# Get job ID by name (critical for trace operations)
gitlab_get_job_id() {
  local pipeline_id="$1"
  local job_name="$2"

  gitlab_get_job_by_name "$pipeline_id" "$job_name" | jq -r '.id // empty'
}
```

### Job Log Operations

```bash
# Get job trace/logs (using numeric job ID)
gitlab_get_job_trace() {
  local job_id="$1"

  if [[ -z "$job_id" ]]; then
    echo "❌ Job ID required"
    return 1
  fi

  # Note: This endpoint returns plain text, not JSON
  gitlab_api_request GET "projects/$CI_PROJECT_ID_ENCODED/jobs/$job_id/trace"
}

# Get job logs by name (resolves name to ID first)
gitlab_get_job_logs_by_name() {
  local pipeline_id="$1"
  local job_name="$2"

  # Get job ID from name
  local job_id=$(gitlab_get_job_id "$pipeline_id" "$job_name")

  if [[ -z "$job_id" ]]; then
    echo "❌ Job '$job_name' not found in pipeline $pipeline_id"
    return 1
  fi

  echo "📋 Getting logs for job '$job_name' (ID: $job_id)..."
  gitlab_get_job_trace "$job_id"
}

# Get logs for all failed jobs in pipeline
gitlab_get_failed_job_logs() {
  local pipeline_id="$1"

  if [[ -z "$pipeline_id" ]]; then
    pipeline_id=$(gitlab_get_latest_pipeline | jq -r '.id // empty')
    if [[ -z "$pipeline_id" ]]; then
      echo "❌ No pipeline found"
      return 1
    fi
  fi

  local failed_jobs=$(gitlab_get_pipeline_jobs "$pipeline_id" | jq -r '.[] | select(.status == "failed") | "\(.id):\(.name)"')

  if [[ -z "$failed_jobs" ]]; then
    echo "✅ No failed jobs found"
    return 0
  fi

  while IFS=: read -r job_id job_name; do
    echo "=== Failed Job: $job_name (ID: $job_id) ==="
    gitlab_get_job_trace "$job_id" | tail -100
    echo ""
  done <<< "$failed_jobs"
}
```

### Job Control Operations

```bash
# Retry job
gitlab_retry_job() {
  local job_id="$1"

  if [[ -z "$job_id" ]]; then
    echo "❌ Job ID required"
    return 1
  fi

  gitlab_api_request POST "projects/$CI_PROJECT_ID_ENCODED/jobs/$job_id/retry"
}

# Cancel job
gitlab_cancel_job() {
  local job_id="$1"

  if [[ -z "$job_id" ]]; then
    echo "❌ Job ID required"
    return 1
  fi

  gitlab_api_request POST "projects/$CI_PROJECT_ID_ENCODED/jobs/$job_id/cancel"
}

# Play manual job
gitlab_play_job() {
  local job_id="$1"

  if [[ -z "$job_id" ]]; then
    echo "❌ Job ID required"
    return 1
  fi

  gitlab_api_request POST "projects/$CI_PROJECT_ID_ENCODED/jobs/$job_id/play"
}
```

## Artifact Operations

```bash
# Download job artifacts
gitlab_download_artifacts() {
  local job_id="$1"
  local output_file="${2:-artifacts.zip}"

  if [[ -z "$job_id" ]]; then
    echo "❌ Job ID required"
    return 1
  fi

  gitlab_api_init || return 1

  curl -s -H "Authorization: Bearer $GITLAB_TOKEN" \
    -o "$output_file" \
    "$GITLAB_HOST/api/v4/projects/$CI_PROJECT_ID_ENCODED/jobs/$job_id/artifacts"

  if [[ -f "$output_file" ]]; then
    echo "✅ Artifacts downloaded to: $output_file"
  else
    echo "❌ Failed to download artifacts"
    return 1
  fi
}

# Get artifact file content
gitlab_get_artifact_file() {
  local job_id="$1"
  local file_path="$2"

  if [[ -z "$job_id" ]] || [[ -z "$file_path" ]]; then
    echo "❌ Job ID and file path required"
    return 1
  fi

  gitlab_api_request GET "projects/$CI_PROJECT_ID_ENCODED/jobs/$job_id/artifacts/$file_path"
}
```

## Utility Functions

### Response Parsing & Formatting

```bash
# Pretty print pipeline summary
gitlab_print_pipeline_summary() {
  local pipeline_id="$1"

  local info=$(gitlab_get_pipeline_status "$pipeline_id")

  echo "$info" | jq -r '
    "Pipeline #\(.id) - \(.status)
    Branch: \(.ref)
    Started: \(.created_at)
    Duration: \(.duration // 0)s
    Web URL: \(.web_url)

    Jobs:
    \(.jobs[] | "  - \(.name): \(.status) (\(.stage))")"
  '
}

# Format job list
gitlab_format_job_list() {
  jq -r '.[] | "\(.id)\t\(.name)\t\(.status)\t\(.stage)"'
}

# Check if pipeline/job is in progress
gitlab_is_running() {
  local status="$1"
  [[ "$status" == "running" ]] || [[ "$status" == "pending" ]]
}
```

### Error Handling & Validation

```bash
# Validate API response
gitlab_validate_response() {
  local response="$1"

  if [[ -z "$response" ]]; then
    echo "❌ Empty response from GitLab API"
    return 1
  fi

  # Check for error messages
  if echo "$response" | jq -e '.message // .error' >/dev/null 2>&1; then
    echo "❌ API Error: $(echo "$response" | jq -r '.message // .error')"
    return 1
  fi

  return 0
}

# Retry API request with exponential backoff
gitlab_api_retry() {
  local max_attempts=3
  local attempt=1
  local wait_time=1

  while [[ $attempt -le $max_attempts ]]; do
    local response=$(gitlab_api_request "$@")

    if gitlab_validate_response "$response"; then
      echo "$response"
      return 0
    fi

    if [[ $attempt -lt $max_attempts ]]; then
      echo "⏳ Retry attempt $attempt/$max_attempts in ${wait_time}s..." >&2
      sleep $wait_time
      wait_time=$((wait_time * 2))
    fi

    attempt=$((attempt + 1))
  done

  echo "❌ API request failed after $max_attempts attempts"
  return 1
}
```

## Complete Example Workflows

### Monitor Pipeline Until Completion

```bash
gitlab_monitor_pipeline() {
  local pipeline_id="$1"
  local interval="${2:-30}"

  if [[ -z "$pipeline_id" ]]; then
    pipeline_id=$(gitlab_get_latest_pipeline | jq -r '.id // empty')
    if [[ -z "$pipeline_id" ]]; then
      echo "❌ No pipeline found"
      return 1
    fi
  fi

  echo "📊 Monitoring pipeline $pipeline_id..."

  while true; do
    local status=$(gitlab_get_pipeline "$pipeline_id" | jq -r '.status')

    echo "$(date '+%Y-%m-%d %H:%M:%S') - Status: $status"

    if ! gitlab_is_running "$status"; then
      echo "✅ Pipeline completed with status: $status"

      if [[ "$status" == "failed" ]]; then
        echo "❌ Getting failed job logs..."
        gitlab_get_failed_job_logs "$pipeline_id"
      fi

      break
    fi

    sleep "$interval"
  done
}
```

### Debug Failed Pipeline

```bash
gitlab_debug_pipeline() {
  local pipeline_id="${1:-$(gitlab_get_latest_pipeline | jq -r '.id // empty')}"

  if [[ -z "$pipeline_id" ]]; then
    echo "❌ No pipeline found"
    return 1
  fi

  echo "🔍 Debugging pipeline $pipeline_id..."

  # Get pipeline summary
  gitlab_print_pipeline_summary "$pipeline_id"

  # Get failed jobs
  local failed_jobs=$(gitlab_get_pipeline_jobs "$pipeline_id" | jq -r '.[] | select(.status == "failed") | .id')

  if [[ -n "$failed_jobs" ]]; then
    echo -e "\n❌ Failed Jobs:"
    for job_id in $failed_jobs; do
      local job_info=$(gitlab_api_request GET "projects/$CI_PROJECT_ID_ENCODED/jobs/$job_id")
      echo -e "\nJob: $(echo "$job_info" | jq -r '.name') (ID: $job_id)"
      echo "Stage: $(echo "$job_info" | jq -r '.stage')"
      echo "Failure Reason: $(echo "$job_info" | jq -r '.failure_reason // "Unknown"')"
      echo -e "\nLast 50 lines of logs:"
      gitlab_get_job_trace "$job_id" | tail -50
    done
  else
    echo "✅ No failed jobs found"
  fi
}
```

## Usage Examples

```bash
# Initialize API (required before other operations)
gitlab_api_init

# Get latest pipeline status
gitlab_get_latest_pipeline | jq -r '.status'

# Get job logs by name
gitlab_get_job_logs_by_name "1909685353" "test-job"

# Monitor pipeline until completion
gitlab_monitor_pipeline "1909685353"

# Debug failed pipeline
gitlab_debug_pipeline

# Retry failed pipeline
gitlab_retry_pipeline "1909685353"

# Download artifacts from specific job
gitlab_download_artifacts "12345678" "my-artifacts.zip"
```

## Environment Variables

Required:

- `GITLAB_TOKEN` - Personal access token or CI job token
- `CI_PROJECT_ID` - Project ID (auto-detected in GitLab CI)

Optional:

- `GITLAB_HOST` - GitLab instance URL (default: https://gitlab.com)
- `GITLAB_DEBUG` - Enable debug output (set to "true")

## Notes

- All functions use the GitLab REST API v4
- Authentication is handled via Bearer token in headers
- Project ID is URL-encoded to handle namespace projects
- Most endpoints return JSON except trace/logs (plain text)
- Rate limiting: GitLab API has rate limits, use retry logic for production
- Always validate responses before processing
