Add first script
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
commit
caade47b4c
4 changed files with 543 additions and 0 deletions
484
gitlab-job-status
Executable file
484
gitlab-job-status
Executable file
|
|
@ -0,0 +1,484 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
POLL_INTERVAL="${POLL_INTERVAL:-10}"
|
||||
GITLAB_TOKEN="${GITLAB_TOKEN:-}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
DIM='\033[2m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Status symbols
|
||||
SYM_SUCCESS="✓"
|
||||
SYM_FAILED="✗"
|
||||
SYM_RUNNING="●"
|
||||
SYM_PENDING="○"
|
||||
SYM_SKIPPED="⊘"
|
||||
SYM_CANCELED="⊗"
|
||||
SYM_MANUAL="▶"
|
||||
|
||||
log_info() { echo -e "${BLUE}ℹ${NC} $*"; }
|
||||
log_success() { echo -e "${GREEN}✓${NC} $*"; }
|
||||
log_error() { echo -e "${RED}✗${NC} $*" >&2; }
|
||||
|
||||
# Track how many lines we printed for the job display
|
||||
DISPLAY_LINES=0
|
||||
|
||||
# Extract GitLab host and project path from git remote
|
||||
parse_git_remote() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null) || {
|
||||
log_error "Failed to get git remote 'origin'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
local gitlab_host project_path
|
||||
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/(.+?)(\.git)?$ ]]; then
|
||||
# https://gitlab.example.com/group/project.git
|
||||
gitlab_host="${BASH_REMATCH[1]}"
|
||||
project_path="${BASH_REMATCH[2]}"
|
||||
elif [[ "$remote_url" =~ ^ssh://[^@]+@([^:/]+)(:[0-9]+)?/(.+?)(\.git)?$ ]]; then
|
||||
# ssh://git@gitlab.example.com:8080/group/project.git
|
||||
# Port is for SSH, not the HTTPS API, so we discard it
|
||||
gitlab_host="${BASH_REMATCH[1]}"
|
||||
project_path="${BASH_REMATCH[3]}"
|
||||
elif [[ "$remote_url" =~ ^git@([^:]+):(.+?)(\.git)?$ ]]; then
|
||||
# git@gitlab.example.com:group/project.git
|
||||
gitlab_host="${BASH_REMATCH[1]}"
|
||||
project_path="${BASH_REMATCH[2]}"
|
||||
else
|
||||
log_error "Could not parse git remote URL: $remote_url"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove trailing .git if present
|
||||
project_path="${project_path%.git}"
|
||||
|
||||
echo "$gitlab_host"
|
||||
echo "$project_path"
|
||||
}
|
||||
|
||||
# URL-encode a string
|
||||
urlencode() {
|
||||
jq -rn --arg s "$1" '$s | @uri'
|
||||
}
|
||||
|
||||
# Make GitLab API request
|
||||
gitlab_api() {
|
||||
local endpoint="$1"
|
||||
local args=(-s -f)
|
||||
|
||||
if [[ -n "$GITLAB_TOKEN" ]]; then
|
||||
args+=(-H "PRIVATE-TOKEN: $GITLAB_TOKEN")
|
||||
fi
|
||||
|
||||
curl "${args[@]}" "$endpoint"
|
||||
}
|
||||
|
||||
# Get current branch name
|
||||
get_current_branch() {
|
||||
git symbolic-ref --short HEAD 2>/dev/null || {
|
||||
log_error "Not on a branch (detached HEAD?)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Find MR for the current branch
|
||||
find_mr() {
|
||||
local api_base="$1"
|
||||
local project_encoded="$2"
|
||||
local branch="$3"
|
||||
|
||||
local response
|
||||
response=$(gitlab_api "${api_base}/projects/${project_encoded}/merge_requests?source_branch=${branch}&state=opened") || {
|
||||
log_error "Failed to fetch merge requests. Do you need to set GITLAB_TOKEN?"
|
||||
exit 1
|
||||
}
|
||||
|
||||
local mr_count
|
||||
mr_count=$(echo "$response" | jq 'length')
|
||||
|
||||
if [[ "$mr_count" -eq 0 ]]; then
|
||||
log_error "No open MR found for branch '$branch'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$response" | jq '.[0]'
|
||||
}
|
||||
|
||||
# Get pipeline status for an MR
|
||||
get_pipeline() {
|
||||
local api_base="$1"
|
||||
local project_encoded="$2"
|
||||
local mr_iid="$3"
|
||||
|
||||
local response
|
||||
response=$(gitlab_api "${api_base}/projects/${project_encoded}/merge_requests/${mr_iid}/pipelines") || {
|
||||
log_error "Failed to fetch pipelines"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get the most recent pipeline
|
||||
echo "$response" | jq '.[0] // empty'
|
||||
}
|
||||
|
||||
# Get detailed pipeline info
|
||||
get_pipeline_details() {
|
||||
local api_base="$1"
|
||||
local project_encoded="$2"
|
||||
local pipeline_id="$3"
|
||||
|
||||
gitlab_api "${api_base}/projects/${project_encoded}/pipelines/${pipeline_id}"
|
||||
}
|
||||
|
||||
# Get jobs for a pipeline
|
||||
get_pipeline_jobs() {
|
||||
local api_base="$1"
|
||||
local project_encoded="$2"
|
||||
local pipeline_id="$3"
|
||||
|
||||
gitlab_api "${api_base}/projects/${project_encoded}/pipelines/${pipeline_id}/jobs?per_page=100"
|
||||
}
|
||||
|
||||
# Check if pipeline is still running
|
||||
is_pipeline_running() {
|
||||
local status="$1"
|
||||
[[ "$status" == "running" || "$status" == "pending" || "$status" == "created" || "$status" == "waiting_for_resource" || "$status" == "preparing" ]]
|
||||
}
|
||||
|
||||
# Print failed jobs
|
||||
print_failed_jobs() {
|
||||
local gitlab_host="$1"
|
||||
local project_path="$2"
|
||||
local jobs_json="$3"
|
||||
|
||||
local failed_jobs
|
||||
failed_jobs=$(echo "$jobs_json" | jq -r '.[] | select(.status == "failed")')
|
||||
|
||||
if [[ -z "$failed_jobs" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo
|
||||
log_error "Failed jobs:"
|
||||
echo "$jobs_json" | jq -r '.[] | select(.status == "failed") | " - \(.name) (stage: \(.stage))"'
|
||||
echo
|
||||
echo "Job links:"
|
||||
echo "$jobs_json" | jq -r --arg host "$gitlab_host" --arg project "$project_path" \
|
||||
'.[] | select(.status == "failed") | " \(.name): https://\($host)/\($project)/-/jobs/\(.id)"'
|
||||
}
|
||||
|
||||
# Get status symbol and color for a job status
|
||||
get_status_display() {
|
||||
local status="$1"
|
||||
case "$status" in
|
||||
success)
|
||||
echo -e "${GREEN}${SYM_SUCCESS}${NC}"
|
||||
;;
|
||||
failed)
|
||||
echo -e "${RED}${SYM_FAILED}${NC}"
|
||||
;;
|
||||
running)
|
||||
echo -e "${YELLOW}${SYM_RUNNING}${NC}"
|
||||
;;
|
||||
pending|waiting_for_resource|created)
|
||||
echo -e "${DIM}${SYM_PENDING}${NC}"
|
||||
;;
|
||||
skipped)
|
||||
echo -e "${DIM}${SYM_SKIPPED}${NC}"
|
||||
;;
|
||||
canceled)
|
||||
echo -e "${RED}${SYM_CANCELED}${NC}"
|
||||
;;
|
||||
manual)
|
||||
echo -e "${CYAN}${SYM_MANUAL}${NC}"
|
||||
;;
|
||||
preparing)
|
||||
echo -e "${BLUE}${SYM_RUNNING}${NC}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${DIM}?${NC}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Get color for job name based on status
|
||||
get_name_color() {
|
||||
local status="$1"
|
||||
case "$status" in
|
||||
success)
|
||||
echo -e "${GREEN}"
|
||||
;;
|
||||
failed)
|
||||
echo -e "${RED}"
|
||||
;;
|
||||
running|preparing)
|
||||
echo -e "${YELLOW}${BOLD}"
|
||||
;;
|
||||
pending|waiting_for_resource|created)
|
||||
echo -e "${DIM}"
|
||||
;;
|
||||
skipped|canceled)
|
||||
echo -e "${DIM}"
|
||||
;;
|
||||
manual)
|
||||
echo -e "${CYAN}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${NC}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Display jobs grouped by stage
|
||||
display_jobs() {
|
||||
local jobs_json="$1"
|
||||
local pipeline_status="$2"
|
||||
local gitlab_host="$3"
|
||||
local project_path="$4"
|
||||
|
||||
local lines=()
|
||||
|
||||
# Get stages in the order they appear (preserving pipeline order, not alphabetical)
|
||||
# Reverse so earliest stages appear at top
|
||||
local stages
|
||||
stages=$(echo "$jobs_json" | jq -r '
|
||||
reduce .[].stage as $s ([]; if IN(.[]; $s) then . else . + [$s] end) | reverse | .[]
|
||||
')
|
||||
|
||||
while IFS= read -r stage; do
|
||||
[[ -z "$stage" ]] && continue
|
||||
|
||||
# Determine stage status based on its jobs
|
||||
local stage_status
|
||||
stage_status=$(echo "$jobs_json" | jq -r --arg stage "$stage" '
|
||||
[.[] | select(.stage == $stage) | .status] |
|
||||
if any(. == "running" or . == "preparing") then "running"
|
||||
elif any(. == "failed") then "failed"
|
||||
elif any(. == "pending" or . == "created" or . == "waiting_for_resource") then "pending"
|
||||
elif all(. == "success") then "success"
|
||||
elif all(. == "skipped" or . == "success") then "success"
|
||||
else "other"
|
||||
end
|
||||
')
|
||||
|
||||
local stage_color
|
||||
case "$stage_status" in
|
||||
success) stage_color="${GREEN}" ;;
|
||||
failed) stage_color="${RED}" ;;
|
||||
running) stage_color="${YELLOW}" ;;
|
||||
*) stage_color="${DIM}" ;;
|
||||
esac
|
||||
|
||||
lines+=("${stage_color}${BOLD}━━━ ${stage} ━━━${NC}")
|
||||
|
||||
# Get jobs for this stage
|
||||
local stage_jobs
|
||||
stage_jobs=$(echo "$jobs_json" | jq -c --arg stage "$stage" '[.[] | select(.stage == $stage)] | sort_by(.name)')
|
||||
|
||||
local job_count
|
||||
job_count=$(echo "$stage_jobs" | jq 'length')
|
||||
|
||||
for ((j = 0; j < job_count; j++)); do
|
||||
local job
|
||||
job=$(echo "$stage_jobs" | jq -c ".[$j]")
|
||||
|
||||
local job_name job_status job_id
|
||||
job_name=$(echo "$job" | jq -r '.name')
|
||||
job_status=$(echo "$job" | jq -r '.status')
|
||||
job_id=$(echo "$job" | jq -r '.id')
|
||||
|
||||
local job_url="https://${gitlab_host}/${project_path}/-/jobs/${job_id}"
|
||||
|
||||
local status_sym name_color
|
||||
status_sym=$(get_status_display "$job_status")
|
||||
name_color=$(get_name_color "$job_status")
|
||||
|
||||
# OSC 8 hyperlink: \e]8;;URL\aTEXT\e]8;;\a
|
||||
local linked_name="\e]8;;${job_url}\a${name_color}${job_name}${NC}\e]8;;\a"
|
||||
|
||||
local duration_str=""
|
||||
if [[ "$job_status" == "success" || "$job_status" == "failed" ]]; then
|
||||
local duration
|
||||
duration=$(echo "$job" | jq -r '.duration // 0 | floor')
|
||||
if [[ "$duration" -gt 0 ]]; then
|
||||
local mins=$((duration / 60))
|
||||
local secs=$((duration % 60))
|
||||
if [[ $mins -gt 0 ]]; then
|
||||
duration_str=" ${DIM}(${mins}m ${secs}s)${NC}"
|
||||
else
|
||||
duration_str=" ${DIM}(${secs}s)${NC}"
|
||||
fi
|
||||
fi
|
||||
elif [[ "$job_status" == "running" ]]; then
|
||||
local started_at
|
||||
started_at=$(echo "$job" | jq -r '.started_at // empty')
|
||||
if [[ -n "$started_at" ]]; then
|
||||
local start_epoch now_epoch elapsed
|
||||
start_epoch=$(date -d "$started_at" +%s 2>/dev/null) || start_epoch=""
|
||||
if [[ -n "$start_epoch" ]]; then
|
||||
now_epoch=$(date +%s)
|
||||
elapsed=$((now_epoch - start_epoch))
|
||||
local mins=$((elapsed / 60))
|
||||
local secs=$((elapsed % 60))
|
||||
if [[ $mins -gt 0 ]]; then
|
||||
duration_str=" ${DIM}(${mins}m ${secs}s)${NC}"
|
||||
else
|
||||
duration_str=" ${DIM}(${secs}s)${NC}"
|
||||
fi
|
||||
else
|
||||
duration_str=" ${DIM}(running...)${NC}"
|
||||
fi
|
||||
else
|
||||
duration_str=" ${DIM}(running...)${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
lines+=(" ${status_sym} ${linked_name}${duration_str}")
|
||||
done
|
||||
|
||||
lines+=("") # Empty line between stages
|
||||
|
||||
done <<< "$stages"
|
||||
|
||||
# Add pipeline status line
|
||||
local status_line
|
||||
case "$pipeline_status" in
|
||||
running|pending|created|waiting_for_resource|preparing)
|
||||
status_line="${YELLOW}⏳ Pipeline ${pipeline_status}...${NC}"
|
||||
;;
|
||||
success)
|
||||
status_line="${GREEN}✓ Pipeline succeeded${NC}"
|
||||
;;
|
||||
failed)
|
||||
status_line="${RED}✗ Pipeline failed${NC}"
|
||||
;;
|
||||
*)
|
||||
status_line="Pipeline: ${pipeline_status}"
|
||||
;;
|
||||
esac
|
||||
lines+=("$status_line")
|
||||
|
||||
# Build complete output string
|
||||
local output=""
|
||||
local new_line_count=0
|
||||
for line in "${lines[@]}"; do
|
||||
output+="$line"$'\n'
|
||||
new_line_count=$((new_line_count + 1))
|
||||
done
|
||||
|
||||
# Atomic clear and redraw: cursor up + clear to end + new content
|
||||
local clear_seq=""
|
||||
if [[ $DISPLAY_LINES -gt 0 ]]; then
|
||||
clear_seq="$(tput cuu "$DISPLAY_LINES")$(tput ed)"
|
||||
fi
|
||||
|
||||
echo -en "${clear_seq}${output}"
|
||||
DISPLAY_LINES=$new_line_count
|
||||
}
|
||||
|
||||
main() {
|
||||
log_info "Parsing git remote..."
|
||||
local remote_info
|
||||
remote_info=$(parse_git_remote)
|
||||
local gitlab_host project_path
|
||||
gitlab_host=$(echo "$remote_info" | sed -n '1p')
|
||||
project_path=$(echo "$remote_info" | sed -n '2p')
|
||||
|
||||
local api_base="https://${gitlab_host}/api/v4"
|
||||
local project_encoded
|
||||
project_encoded=$(urlencode "$project_path")
|
||||
|
||||
log_info "GitLab: ${gitlab_host}"
|
||||
log_info "Project: ${project_path}"
|
||||
|
||||
local branch
|
||||
branch=$(get_current_branch)
|
||||
log_info "Branch: ${branch}"
|
||||
|
||||
log_info "Finding merge request..."
|
||||
local mr
|
||||
mr=$(find_mr "$api_base" "$project_encoded" "$branch")
|
||||
|
||||
local mr_iid mr_title mr_url
|
||||
mr_iid=$(echo "$mr" | jq -r '.iid')
|
||||
mr_title=$(echo "$mr" | jq -r '.title')
|
||||
mr_url=$(echo "$mr" | jq -r '.web_url')
|
||||
|
||||
log_success "Found MR !${mr_iid}: ${mr_title}"
|
||||
log_info "URL: ${mr_url}"
|
||||
|
||||
log_info "Checking pipeline status..."
|
||||
local pipeline
|
||||
pipeline=$(get_pipeline "$api_base" "$project_encoded" "$mr_iid")
|
||||
|
||||
if [[ -z "$pipeline" || "$pipeline" == "null" ]]; then
|
||||
log_error "No pipeline found for this MR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local pipeline_id pipeline_status
|
||||
pipeline_id=$(echo "$pipeline" | jq -r '.id')
|
||||
pipeline_status=$(echo "$pipeline" | jq -r '.status')
|
||||
|
||||
log_info "Pipeline #${pipeline_id} - Status: ${pipeline_status}"
|
||||
echo
|
||||
|
||||
# Initial job display
|
||||
local jobs
|
||||
jobs=$(get_pipeline_jobs "$api_base" "$project_encoded" "$pipeline_id")
|
||||
display_jobs "$jobs" "$pipeline_status" "$gitlab_host" "$project_path"
|
||||
|
||||
# Poll until pipeline completes
|
||||
while is_pipeline_running "$pipeline_status"; do
|
||||
sleep "$POLL_INTERVAL"
|
||||
|
||||
local pipeline_details
|
||||
pipeline_details=$(get_pipeline_details "$api_base" "$project_encoded" "$pipeline_id")
|
||||
pipeline_status=$(echo "$pipeline_details" | jq -r '.status')
|
||||
|
||||
jobs=$(get_pipeline_jobs "$api_base" "$project_encoded" "$pipeline_id")
|
||||
display_jobs "$jobs" "$pipeline_status" "$gitlab_host" "$project_path"
|
||||
done
|
||||
|
||||
echo # New line after the status updates
|
||||
|
||||
# Get final pipeline info
|
||||
local pipeline_details pipeline_url
|
||||
pipeline_details=$(get_pipeline_details "$api_base" "$project_encoded" "$pipeline_id")
|
||||
pipeline_url=$(echo "$pipeline_details" | jq -r '.web_url')
|
||||
|
||||
echo
|
||||
log_info "Pipeline URL: ${pipeline_url}"
|
||||
|
||||
# Report final status
|
||||
case "$pipeline_status" in
|
||||
success)
|
||||
exit 0
|
||||
;;
|
||||
failed)
|
||||
print_failed_jobs "$gitlab_host" "$project_path" "$jobs"
|
||||
exit 1
|
||||
;;
|
||||
canceled)
|
||||
exit 2
|
||||
;;
|
||||
skipped)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue