UNPKG

@cloudkinetix/bmad-enhanced

Version:

Cloud-Kinetix enhanced fork of BMAD-METHOD - Breakthrough Method of Agile AI-driven Development with robust versioning and unified validation.

564 lines (429 loc) 13.4 kB
# 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