@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
Markdown
# 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