@@ -34,7 +34,7 @@ usage() {
34
34
cat<< EOF
35
35
Usage:
36
36
$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]
38
38
39
39
Required:
40
40
-h GitLab host (e.g. gitlab.example.com)
@@ -50,15 +50,18 @@ Authentication (one of):
50
50
Scope (choose one):
51
51
-p Project ID or full path (e.g. 42 or group/app)
52
52
-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)
53
56
EOF
54
57
exit 1
55
58
}
56
59
57
60
HOST=" " HOOK_URL=" " HOOK_SECRET=" "
58
61
TOKEN=" ${GITLAB_TOKEN:- } " AUTH_HEADER=" "
59
- PROJECT=" " GROUP=" "
62
+ PROJECT=" " GROUP=" " VERBOSE=false
60
63
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
62
65
case " $opt " in
63
66
h) HOST=$OPTARG ;;
64
67
u) HOOK_URL=$OPTARG ;;
@@ -67,6 +70,7 @@ while getopts "h:u:s:t:A:p:g:" opt; do
67
70
A) AUTH_HEADER=$OPTARG ;;
68
71
p) PROJECT=$OPTARG ;;
69
72
g) GROUP=$OPTARG ;;
73
+ v) VERBOSE=true ;;
70
74
* ) usage ;;
71
75
esac
72
76
done
78
82
79
83
# Token handling
80
84
if [[-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
82
86
exit 1
83
87
fi
84
88
@@ -98,6 +102,11 @@ CURL_BASE=(curl -sSf --header "${AUTH_HEADER}: ${TOKEN}")
98
102
declare -A PROCESSED_PROJECTS
99
103
# Track projects where webhooks were successfully added
100
104
WEBHOOK_PROJECTS=()
105
+ # Track projects where webhooks already existed
106
+ EXISTING_WEBHOOK_PROJECTS=()
107
+ # Progress counters
108
+ TOTAL_PROJECTS_FOUND=0
109
+ PROJECTS_PROCESSED=0
101
110
102
111
# #############################################################################
103
112
# Helpers
@@ -108,6 +117,63 @@ url_encode() {
108
117
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'
109
118
}
110
119
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
+
111
177
create_hook () {
112
178
local pid=$1
113
179
@@ -118,6 +184,7 @@ create_hook() {
118
184
119
185
# Mark as processed
120
186
PROCESSED_PROJECTS[$pid ]=1
187
+ PROJECTS_PROCESSED=$(( PROJECTS_PROCESSED+ 1 ))
121
188
122
189
local encoded_pid
123
190
# URL encode if pid is not purely numeric
@@ -127,6 +194,22 @@ create_hook() {
127
194
encoded_pid=$( url_encode" $pid " )
128
195
fi
129
196
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/null2>&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
+
130
213
" ${CURL_BASE[@]} " --request POST \
131
214
--data-urlencode" url=${HOOK_URL} " \
132
215
--data" token=${HOOK_SECRET} " \
@@ -151,38 +234,64 @@ traverse_group() {
151
234
else
152
235
encoded_gid=$( url_encode" $gid " )
153
236
fi
154
- # projects (includes nested sub-groups)
237
+
238
+ # projects (includes nested sub-groups) - with pagination
155
239
while IFS=read -r pid; do
156
240
[[-n " $pid " ]]&& create_hook" $pid "
157
241
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"
161
243
)
162
- # recurse explicit subgroups (older GitLab)
244
+
245
+ # recurse explicit subgroups (older GitLab) - with pagination
163
246
while IFS=read -r sg; do
164
247
[[-n " $sg " ]]&& traverse_group" $sg "
165
248
done < <(
166
- " ${CURL_BASE[@]} " " ${API} /groups/${encoded_gid} /subgroups?per_page=100" |
167
- jq -r' .[].id'
249
+ fetch_paginated" ${API} /groups/${encoded_gid} /subgroups"
168
250
)
169
251
}
170
252
171
253
# #############################################################################
172
254
# Main
173
255
# #############################################################################
256
+ echo " [INFO] Starting webhook processing..." >&2
257
+
174
258
if [[-n $PROJECT ]]; then
259
+ echo " [INFO] Processing single project:$PROJECT " >&2
175
260
create_hook" $PROJECT "
176
261
else
262
+ echo " [INFO] Processing group and subgroups:$GROUP " >&2
177
263
traverse_group" $GROUP "
178
264
fi
179
265
266
+ echo " [INFO] Finished processing all projects" >&2
267
+
180
268
# 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"
183
273
else
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 "
188
297
fi