@@ -34,7 +34,7 @@ usage() {
3434 cat << EOF
3535Usage:
3636 $0 -h <gitlab-host> -u <webhook-url> -s <webhook-secret> \\
37- [-t <access-token>] [-A <auth-header>] [-p <project> | -g <group>]
37+ [-t <access-token>] [-A <auth-header>] [-p <project> | -g <group>] [-v]
3838
3939Required:
4040 -h GitLab host (e.g. gitlab.example.com)
@@ -50,15 +50,18 @@ Authentication (one of):
5050Scope (choose one):
5151 -p Project ID or full path (e.g. 42 or group/app)
5252 -g Group ID or full path, recurse through all subgroups & projects
53+
54+ Options:
55+ -v Verbose output (show individual project IDs in final summary)
5356EOF
5457 exit 1
5558}
5659
5760HOST=" " HOOK_URL=" " HOOK_SECRET=" "
5861TOKEN=" ${GITLAB_TOKEN:- } " AUTH_HEADER=" "
59- PROJECT=" " GROUP=" "
62+ PROJECT=" " GROUP=" " VERBOSE=false
6063
61- while getopts " h:u:s:t:A:p:g:" opt; do
64+ while getopts " h:u:s:t:A:p:g:v " opt; do
6265 case " $opt " in
6366 h) HOST=$OPTARG ;;
6467 u) HOOK_URL=$OPTARG ;;
@@ -67,6 +70,7 @@ while getopts "h:u:s:t:A:p:g:" opt; do
6770 A) AUTH_HEADER=$OPTARG ;;
6871 p) PROJECT=$OPTARG ;;
6972 g) GROUP=$OPTARG ;;
73+ v) VERBOSE=true ;;
7074 * ) usage ;;
7175 esac
7276done
7882
7983# Token handling
8084if [[ -z $TOKEN ]]; then
81- echo " ❌ No access token provided. Use -t or set \$ GITLAB_TOKEN" >&2
85+ echo " [ERROR] No access token provided. Use -t or set \$ GITLAB_TOKEN" >&2
8286 exit 1
8387fi
8488
@@ -98,6 +102,11 @@ CURL_BASE=(curl -sSf --header "${AUTH_HEADER}: ${TOKEN}")
98102declare -A PROCESSED_PROJECTS
99103# Track projects where webhooks were successfully added
100104WEBHOOK_PROJECTS=()
105+ # Track projects where webhooks already existed
106+ EXISTING_WEBHOOK_PROJECTS=()
107+ # Progress counters
108+ TOTAL_PROJECTS_FOUND=0
109+ PROJECTS_PROCESSED=0
101110
102111# #############################################################################
103112# Helpers
@@ -108,6 +117,63 @@ url_encode() {
108117 printf ' %s' " $string " | sed ' s/\//%2F/g; s/ /%20/g; s/@/%40/g; s/:/%3A/g; s/#/%23/g; s/?/%3F/g; s/&/%26/g; s/=/%3D/g; s/+/%2B/g'
109118}
110119
120+ # Function to handle paginated API calls
121+ fetch_paginated () {
122+ local url=$1
123+ local page=1
124+ local per_page=100
125+
126+ while true ; do
127+ local paginated_url=" ${url} ?per_page=${per_page} &page=${page} "
128+
129+ # Add existing query params if they exist
130+ if [[ " $url " == * " ?" * ]]; then
131+ paginated_url=" ${url} &per_page=${per_page} &page=${page} "
132+ fi
133+
134+ local response
135+ response=$( " ${CURL_BASE[@]} " " $paginated_url " 2> /dev/null) || {
136+ echo " [ERROR] Failed to fetch page $page from $url " >&2
137+ return 1
138+ }
139+
140+ # Check if response is empty array or null
141+ if [[ " $response " == " []" || " $response " == " null" ]]; then
142+ break
143+ fi
144+
145+ # Extract results from current page
146+ local page_results
147+ page_results=$( echo " $response " | jq -r ' .[].id' 2> /dev/null) || {
148+ echo " [ERROR] Failed to parse JSON response from page $page " >&2
149+ return 1
150+ }
151+
152+ # If no results on this page, we're done
153+ if [[ -z " $page_results " ]]; then
154+ break
155+ fi
156+
157+ # Count projects found and show progress
158+ local page_count
159+ page_count=$( echo " $page_results " | wc -l)
160+ TOTAL_PROJECTS_FOUND=$(( TOTAL_PROJECTS_FOUND + page_count))
161+ echo " [PROGRESS] Found $page_count projects on page $page (total: $TOTAL_PROJECTS_FOUND )" >&2
162+
163+ # Output page results
164+ echo " $page_results "
165+
166+ # If we got less than per_page results, we're on the last page
167+ local item_count
168+ item_count=$( echo " $response " | jq ' . | length' 2> /dev/null) || 0
169+ if [[ " $item_count " -lt " $per_page " ]]; then
170+ break
171+ fi
172+
173+ (( page++ ))
174+ done
175+ }
176+
111177create_hook () {
112178 local pid=$1
113179
@@ -118,6 +184,7 @@ create_hook() {
118184
119185 # Mark as processed
120186 PROCESSED_PROJECTS[$pid ]=1
187+ PROJECTS_PROCESSED=$(( PROJECTS_PROCESSED + 1 ))
121188
122189 local encoded_pid
123190 # URL encode if pid is not purely numeric
@@ -127,6 +194,22 @@ create_hook() {
127194 encoded_pid=$( url_encode " $pid " )
128195 fi
129196
197+ # Check if webhook already exists
198+ local existing_webhooks
199+ existing_webhooks=$( " ${CURL_BASE[@]} " " ${API} /projects/${encoded_pid} /hooks" 2> /dev/null) || {
200+ echo " [ERROR] Failed to fetch existing webhooks for project $pid " >&2
201+ return 1
202+ }
203+
204+ # Check if our webhook URL already exists
205+ if echo " $existing_webhooks " | jq -e --arg url " $HOOK_URL " ' .[] | select(.url == $url)' > /dev/null 2>&1 ; then
206+ [[ " $VERBOSE " == " true" ]] && echo " [INFO] Webhook already exists for project: $pid " >&2
207+ EXISTING_WEBHOOK_PROJECTS+=(" $pid " )
208+ return 0
209+ fi
210+
211+ [[ " $VERBOSE " == " true" ]] && echo " [INFO] Adding webhook to project: $pid " >&2
212+
130213 " ${CURL_BASE[@]} " --request POST \
131214 --data-urlencode " url=${HOOK_URL} " \
132215 --data " token=${HOOK_SECRET} " \
@@ -151,38 +234,64 @@ traverse_group() {
151234 else
152235 encoded_gid=$( url_encode " $gid " )
153236 fi
154- # projects (includes nested sub-groups)
237+
238+ # projects (includes nested sub-groups) - with pagination
155239 while IFS= read -r pid; do
156240 [[ -n " $pid " ]] && create_hook " $pid "
157241 done < <(
158- " ${CURL_BASE[@]} " \
159- " ${API} /groups/${encoded_gid} /projects?include_subgroups=true&per_page=100" |
160- jq -r ' .[].id'
242+ fetch_paginated " ${API} /groups/${encoded_gid} /projects?include_subgroups=true"
161243 )
162- # recurse explicit subgroups (older GitLab)
244+
245+ # recurse explicit subgroups (older GitLab) - with pagination
163246 while IFS= read -r sg; do
164247 [[ -n " $sg " ]] && traverse_group " $sg "
165248 done < <(
166- " ${CURL_BASE[@]} " " ${API} /groups/${encoded_gid} /subgroups?per_page=100" |
167- jq -r ' .[].id'
249+ fetch_paginated " ${API} /groups/${encoded_gid} /subgroups"
168250 )
169251}
170252
171253# #############################################################################
172254# Main
173255# #############################################################################
256+ echo " [INFO] Starting webhook processing..." >&2
257+
174258if [[ -n $PROJECT ]]; then
259+ echo " [INFO] Processing single project: $PROJECT " >&2
175260 create_hook " $PROJECT "
176261else
262+ echo " [INFO] Processing group and subgroups: $GROUP " >&2
177263 traverse_group " $GROUP "
178264fi
179265
266+ echo " [INFO] Finished processing all projects" >&2
267+
180268# Print final summary
181- if [[ ${# WEBHOOK_PROJECTS[@]} -eq 0 ]]; then
182- echo " ❌ No webhooks were installed."
269+ total_projects=$(( ${# WEBHOOK_PROJECTS[@]} + ${# EXISTING_WEBHOOK_PROJECTS[@]} ))
270+
271+ if [[ $total_projects -eq 0 ]]; then
272+ echo " [INFO] No projects were processed"
183273else
184- echo " ✅ Webhooks installed successfully on ${# WEBHOOK_PROJECTS[@]} project(s):"
185- for pid in " ${WEBHOOK_PROJECTS[@]} " ; do
186- echo " - Project ID: $pid "
187- done
274+ if [[ ${# WEBHOOK_PROJECTS[@]} -gt 0 ]]; then
275+ if [[ " $VERBOSE " == " true" ]]; then
276+ echo " [INFO] Webhooks installed successfully on ${# WEBHOOK_PROJECTS[@]} project(s):"
277+ for pid in " ${WEBHOOK_PROJECTS[@]} " ; do
278+ echo " - Project ID: $pid "
279+ done
280+ else
281+ echo " [INFO] Webhooks installed successfully on ${# WEBHOOK_PROJECTS[@]} project(s)"
282+ fi
283+ fi
284+
285+ if [[ ${# EXISTING_WEBHOOK_PROJECTS[@]} -gt 0 ]]; then
286+ if [[ " $VERBOSE " == " true" ]]; then
287+ echo " [INFO] Webhooks already existed on ${# EXISTING_WEBHOOK_PROJECTS[@]} project(s):"
288+ for pid in " ${EXISTING_WEBHOOK_PROJECTS[@]} " ; do
289+ echo " - Project ID: $pid "
290+ done
291+ else
292+ echo " [INFO] Webhooks already existed on ${# EXISTING_WEBHOOK_PROJECTS[@]} project(s)"
293+ fi
294+ fi
295+
296+ echo " [INFO] Total projects processed: $total_projects "
188297fi
0 commit comments