aboutsummaryrefslogtreecommitdiffstats
path: root/tools/webhook_installer/installer.py
blob: 48d5367dad2a3a8d0b304f1a2af6b16a92707b4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

"""Use a passed repo list, or query gerrit for a list of repos, then iterate the list and
# query for the webhooks.config file on refs/meta/config for each repo.
# If the file exists and has a section titled `remote "qt-cherry-pick-bot"`,
# Then update the url of the section to point to the correct url for staging or production.
# If the file does not exist, create it using the template below.
# If the file exists but does not have a section titled `remote "qt-cherry-pick-bot"`,
# Then append the template below to the end of the file.
# Push the changes to gerrit. Publish the change and submit it."""

import sys
import json
import logging
import re
import urllib.request
import urllib.parse
import urllib.error
import base64
import argparse
from os import environ as envs
from urllib.parse import unquote, quote


# Parse arguments
parser = argparse.ArgumentParser(description="Add or update webhooks.config for qt-cherry-pick-bot")
parser.add_argument("--simulate", action="store_true", help="Perform a dry run but make no changes")
parser.add_argument("repos", nargs="*", help="List of repos to update. Defaults to all repos with"
                    " existing webhooks.config", default=[])
parser.add_argument("--add", action="store_true", help="Add webhooks.config to repos that don't"
                    " have it. Ignored if --repos is not specified")
parser.add_argument("--production", action="store_true", help="Update webhooks.config for"
                    " production, defaults to staging if not set")
parser.add_argument("--update-rootconfig", action="store_true", help="Update webhooks.config"
                    " for the 'All-Projects' repository with specific jira-closer-bot and "
                    "gerrit-webhooks-forwarder settings. Other repo arguments are ignored.")
parser.add_argument("--updateConfig", action="store_true", help="Update the entire"
                    " qt-cherry-pick-bot section if it differs from the template")
args = parser.parse_args()


gerrit_url = envs.get("GERRIT_URL", "https://codereview.qt-project.org")
CHERRY_PICK_PROD_URL = "https://qt-cherry-pick-bot.herokuapp.com/gerrit-events"
CHERRY_PICK_STAGE_URL = "https://qt-cherry-pick-bot-staging.herokuapp.com/gerrit-events"
# Gerrit user must have the following permissions:
# - Create change
# - Push
# - Submit
# - Approver
gerrit_username = envs.get("GERRIT_USERNAME", "")
gerrit_password = envs.get("GERRIT_PASSWORD", "")
gerrit_repos_with_webhooks = []
gerrit_repos_with_webhooks_to_update = []
gerrit_repos_with_webhooks_to_update_failed = []
gerrit_repos_with_webhooks_to_update_succeeded = []
gerrit_repos_without_webhooks = []
gerrit_repos_with_webhooks_not_updated = []


# Templates for specific sections in "All-Projects" webhooks.config
JIRA_CLOSER_BOT_TEMPLATE = """[remote "jira-closer-bot"]
  url = {}
  maxTries = 10
  event = change-merged
"""

GERRIT_WEBHOOKS_FORWARDER_PROD_TEMPLATE = """[remote "gerrit-webhooks-forwarder"]
  url = http://10.212.0.77:8080
  maxTries = 10
"""

GERRIT_WEBHOOKS_FORWARDER_STAGING_TEMPLATE = """# [remote "gerrit-webhooks-forwarder"]
#   url = http://10.212.0.77:8080
#   maxTries = 10
"""

CONFIG_SECTION_HEADER = '[remote "qt-cherry-pick-bot"]'
CONFIG_SECTION_HEADER_ESC = r'\[remote "qt-cherry-pick-bot"\]' # Escaped for regex

# Determine target URL and template based on args
TARGET_URL = CHERRY_PICK_PROD_URL if args.production else CHERRY_PICK_STAGE_URL
WEBHOOK_TEMPLATE = f"""{CONFIG_SECTION_HEADER}
  url = {TARGET_URL}
  maxTries = 10
  event = patchset-created
  event = change-merged
  event = change-abandoned
  event = change-staged
  event = change-unstaged
  event = comment-added
  event = change-deferred
  event = change-restored
  event = hashtags-changed
  event = change-integration-pass
  event = change-integration-fail
"""

gerrit_authstring = base64.b64encode(f"{gerrit_username}:{gerrit_password}"
                                     .encode("utf-8")).decode("utf-8")
gerrit_authstring = f"Basic {gerrit_authstring}"


# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# Functions
def get_gerrit_repos():
    """ Get a list of all repos from gerrit."""
    url = f"{gerrit_url}/a/projects/"
    request = urllib.request.Request(url)
    request.add_header("Authorization", gerrit_authstring)
    try:
        with urllib.request.urlopen(request) as response:
            if response.status == 200:
                content = json.loads(response.read().decode("utf-8").replace(")]}'", ""))
                return [content[repo]["id"] for repo in content.keys()
                        if content[repo]["state"] == "ACTIVE"]
            else:
                logger.error("Failed to get list of repos from gerrit: %s %s",
                             response.status, response.reason)
                sys.exit(1)
    except urllib.error.URLError as err:
        logger.error("Failed to connect to gerrit: %s", err.reason)
        sys.exit(1)


def get_gerrit_repo_webhooks(repo):
    """ Get the webhooks.config file from the refs/meta/config branch
    It is returned as a base64 encoded string. Decode before returning."""
    url = f"{gerrit_url}/a/projects/{repo}"
    print("trying", url)
    request = urllib.request.Request(url)
    request.add_header("Authorization", gerrit_authstring)

    try:
        with urllib.request.urlopen(request) as response:
            if response.status == 200:
                logger.info("Repo %s exists", urllib.parse.unquote(repo))
    except urllib.error.URLError as err:
        if err.status == 404:
            logger.error("Repo %s does not exist", urllib.parse.unquote(repo))
            sys.exit(1)
        logger.error("Failed to connect to gerrit: %s %s", err.status, err.reason)
        sys.exit(1)

    url = f"{gerrit_url}/a/projects/{repo}/branches/refs%2Fmeta%2Fconfig" \
          "/files/webhooks.config/content"
    request = urllib.request.Request(url)
    request.add_header("Authorization", gerrit_authstring)

    try:
        with urllib.request.urlopen(request) as response:
            if response.status == 200:
                logger.info("Got webhooks.config file from repo %s", urllib.parse.unquote(repo))
                return base64.b64decode(response.read()).decode("utf-8")
            else:
                logger.info("No webhooks.config file in repo %s: %s %s",
                            urllib.parse.unquote(repo), response.status, response.reason)
                gerrit_repos_without_webhooks.append(repo)
                return None
    except urllib.error.URLError as err:
        if err.status == 404:
            logger.info("No webhooks.config file in repo %s", urllib.parse.unquote(repo))
            gerrit_repos_without_webhooks.append(repo)
            return None
        logger.error("Failed to connect to gerrit: %s %s", err.status, err.reason)
        sys.exit(1)


def update_gerrit_repo_webhooks(repo, target_webhooks_config_content, commit_subject):
    """ Create a new change request with the webhooks.config file as the only file to be changed,
    but only if the target content differs from the current content in Gerrit.
    The change request will be created with the provided commit_subject.
    The new/modified webhooks.config file will be pushed to the change request.
    The change edit will be published.
    The change request will be self-reviewed and submitted.
    """
    logger.info(f"Proceeding with update attempt for webhooks.config in '{unquote(repo)}'.")

    # Create a new change request
    change_input = {"project": unquote(repo),
                    "branch": "refs/meta/config",
                    "subject": commit_subject,
                    "is_work_in_progress": False
                    }
    if args.simulate:
        logger.info("Simulating change request creation for repo %s", unquote(repo))
        logger.info("Change request input: %s", change_input)
        change_request_id = "abcd1234"
    else:
        url = f"{gerrit_url}/a/changes/"
        data = json.dumps(change_input).encode("utf-8")
        request = urllib.request.Request(url, data=data, method="POST")
        request.add_header("Authorization", gerrit_authstring)
        request.add_header("Content-Type", "application/json")
        try:
            with urllib.request.urlopen(request, timeout=60) as response:
                if response.status == 201:
                    change_request = json.loads(response.read().decode("utf-8").replace(")]}'", ""))
                    change_request_id = change_request["id"]
                    logger.info("Created change request %s for repo %s",
                                change_request_id, urllib.parse.unquote(repo))
                else:
                    logger.error("Failed to create change request for repo %s: %s %s",
                                 urllib.parse.unquote(repo), response.status,
                                 response.read().decode("utf-8"))
                    gerrit_repos_with_webhooks_to_update_failed.append(repo)
                    return None
        except urllib.error.URLError as err:
            logger.error(f"Failed to connect to gerrit to create change for {unquote(repo)}:"
                         f" {err.reason}")
            gerrit_repos_with_webhooks_to_update_failed.append(repo)
            return None
        except Exception as e:
            logger.error(f"Unexpected error creating change for {unquote(repo)}: {e}")
            gerrit_repos_with_webhooks_to_update_failed.append(repo)
            return None


    # Upload the webhooks.config file to the change request
    if args.simulate:
        logger.info("Simulating upload of webhooks.config to change request %s for repo %s",
                    change_request_id, unquote(repo))
        logger.info("Webhooks.config: %s", target_webhooks_config_content)
    else:
        url = f"{gerrit_url}/a/changes/{change_request_id}/edit/webhooks.config"
        data = target_webhooks_config_content.encode("utf-8")
        request = urllib.request.Request(url, data=data, method="PUT")
        request.add_header("Authorization", gerrit_authstring)
        request.add_header("Content-Type", "application/octet-stream")
        try:
            with urllib.request.urlopen(request) as response:
                if response.status == 204:
                    logger.info("Uploaded webhooks.config to change request %s for repo %s",
                                change_request_id, urllib.parse.unquote(repo))
                elif response.status == 409: # Conflict - e.g. content is identical
                    logger.info("Webhooks.config for change %s on repo %s is considered"
                                " identical by Gerrit (409 Conflict). No effective update.",
                                change_request_id, urllib.parse.unquote(repo))
                    # This case implies our pre-check found a difference,
                    # but Gerrit's PUT diffing did not.
                    # Treat as not updated in terms of actual change.
                    if repo not in gerrit_repos_with_webhooks_not_updated:
                        gerrit_repos_with_webhooks_not_updated.append(repo)
                    # Consider abandoning the created change if it's now empty or no-op.
                    # This generally shouldn't happen due to earlier checks.
                    return None
                else:
                    logger.error("Failed to upload webhooks.config to change request %s"
                                 " for repo %s: %s %s",
                                 change_request_id, urllib.parse.unquote(repo), response.status,
                                 response.read().decode("utf-8"))
                    gerrit_repos_with_webhooks_to_update_failed.append(repo)
                    return None
        except urllib.error.URLError as err:
            logger.error("Failed to connect to gerrit to upload config for change"
                         f" {change_request_id} on repo {unquote(repo)}: {err.reason}")
            gerrit_repos_with_webhooks_to_update_failed.append(repo)
            return None
        except Exception as e: # Catch any other unexpected errors during upload
            logger.error(f"Unexpected error uploading config for change {change_request_id}"
                         f" on repo {unquote(repo)}: {e}")
            gerrit_repos_with_webhooks_to_update_failed.append(repo)
            return None

    # Publish the change request (only if upload was successful and not a 409)
    if args.simulate:
        logger.info("Simulating publishing change request " + change_request_id
                    + " for repo " + unquote(repo))
    else:
        url = f"{gerrit_url}/a/changes/{change_request_id}/edit:publish"
        request = urllib.request.Request(url, method="POST")
        request.add_header("Authorization", gerrit_authstring)
        try:
            with urllib.request.urlopen(request) as response:
                if response.status == 204:
                    logger.info("Published change request %s for repo %s",
                                change_request_id, urllib.parse.unquote(repo))
                else:
                    logger.error("Failed to publish change request %s for repo %s",
                                 change_request_id, urllib.parse.unquote(repo))
                    logger.error("%s: %s", response.status, response.read().decode("utf-8"))
                    gerrit_repos_with_webhooks_to_update_failed.append(repo)
                    return None
        except urllib.error.URLError as err:
            logger.error("Failed to connect to gerrit: %s", err.reason)
            sys.exit(1)

    # Self-review the change request
    if args.simulate:
        logger.info("Simulating self-reviewing change request " + change_request_id
                    + " for repo " + unquote(repo))
    else:
        url = f"{gerrit_url}/a/changes/{change_request_id}/revisions/current/review"
        data = json.dumps({"labels": {"Code-Review": 2}}).encode("utf-8")
        request = urllib.request.Request(url, data=data, method="POST")
        request.add_header("Authorization", gerrit_authstring)
        request.add_header("Content-Type", "application/json")
        try:
            with urllib.request.urlopen(request) as response:
                if response.status == 200:
                    logger.info("Self-reviewed change request %s for repo %s",
                                change_request_id, urllib.parse.unquote(repo))
                else:
                    logger.error("Failed to self-review change request %s for repo %s",
                                 change_request_id, urllib.parse.unquote(repo))
                    logger.error("%s: %s", response.status, response.read().decode("utf-8"))
                    gerrit_repos_with_webhooks_to_update_failed.append(repo)
                    return None
        except urllib.error.URLError as err:
            logger.error("Failed to connect to gerrit: %s", err.reason)
            sys.exit(1)

    # Submit the change request
    if args.simulate:
        logger.info("Simulating submitting change request " + change_request_id
                    + " for repo " + unquote(repo))
        gerrit_repos_with_webhooks_to_update_succeeded.append(repo)
    else:
        url = f"{gerrit_url}/a/changes/{change_request_id}/submit"
        data = None
        request = urllib.request.Request(url, data=data, method="POST")
        request.add_header("Authorization", gerrit_authstring)
        try:
            with urllib.request.urlopen(request) as response:
                if response.status == 200:
                    logger.info("Submitted change request %s for repo %s",
                                change_request_id, urllib.parse.unquote(repo))
                    gerrit_repos_with_webhooks_to_update_succeeded.append(repo)
                else:
                    logger.error("Failed to submit change request %s for repo %s",
                                 change_request_id, urllib.parse.unquote(repo))
                    logger.error("%s: %s", response.status, response.read().decode("utf-8"))
                    gerrit_repos_with_webhooks_to_update_failed.append(repo)
                    return None
        except urllib.error.URLError as err:
            logger.error("Failed to connect to gerrit: %s", err.reason)
            sys.exit(1)

    return True


def main():
    if args.update_rootconfig:
        repo_name = "All-Projects"
        encoded_repo_name = quote(repo_name, safe='')

        jira_closer_url_prod = "https://qt-cherry-pick-bot.herokuapp.com/jiracloser"
        jira_closer_url_staging = "https://qt-cherry-pick-bot-staging.herokuapp.com/jiracloser"

        if args.production:
            target_jira_section_str = JIRA_CLOSER_BOT_TEMPLATE.format(
                jira_closer_url_prod).strip() + "\n"
            target_forwarder_section_str = GERRIT_WEBHOOKS_FORWARDER_PROD_TEMPLATE.strip() + "\n"
            env_type = "production"
        else:
            target_jira_section_str = JIRA_CLOSER_BOT_TEMPLATE.format(
                jira_closer_url_staging).strip() + "\n"
            target_forwarder_section_str = GERRIT_WEBHOOKS_FORWARDER_STAGING_TEMPLATE.strip() + "\n"
            env_type = "staging"

        logger.info(f"Processing --update-rootconfig for '{repo_name}' with {env_type} settings.")
        original_config_content = get_gerrit_repo_webhooks(encoded_repo_name)

        working_config_content = original_config_content if original_config_content else ""
        config_was_modified = False

        # Define sections to manage: (regex_pattern_to_find_section, target_section_string)
        # The regex needs to match the section header and its content,
        # and for the forwarder, it should match whether it's commented or not.
        sections_to_manage = [
            (r"^((#\s*)?\[remote \"jira-closer-bot\"\]*\n(?:^(?!\[).*\n?)*)",
              target_jira_section_str),
            (r"^((#\s*)?\[remote \"gerrit-webhooks-forwarder\"\]*\n(?:^(?!\[).*\n?)*)",
              target_forwarder_section_str)
        ]

        for pattern_str, target_section_text in sections_to_manage:
            section_regex = re.compile(pattern_str, re.MULTILINE)
            match = section_regex.search(working_config_content)

            if match:
                existing_section_full_text = match.group(0)
                # Compare stripped versions to ignore leading/trailing whitespace differences
                if existing_section_full_text.strip() != target_section_text.strip():
                    # Replace existing section
                    start_index = match.start(0)
                    end_index = match.end(0)
                    working_config_content = working_config_content[:start_index] + target_section_text + "\n" + working_config_content[end_index:]
                    config_was_modified = True
            elif target_section_text.strip(): # Only add if target section is not empty
                # Section not found, append it
                # Ensure proper separation if working_config_content is not empty
                current_stripped = working_config_content.strip()
                if current_stripped:
                    if not working_config_content.endswith("\n"):
                         working_config_content += "\n"
                    working_config_content += "\n"

                working_config_content += target_section_text
                config_was_modified = True

        # Final normalization of the entire potentially modified config
        finalized_config_content = working_config_content.strip()
        if finalized_config_content:
            finalized_config_content += "\n"
        else: # If after all operations, the content is empty (e.g. all target sections were empty)
            finalized_config_content = "" # Ensure it's an empty string, not just whitespace

        # The update_gerrit_repo_webhooks function itself performs a robust comparison.
        # We call it if our section-by-section processing indicates a potential change.
        if config_was_modified:
            # Check if the finalized content is actually different from the original normalized content
            # This avoids an update if changes were only whitespace that got normalized out.
            normalized_original_config = "\n".join(
                line.strip() for line in (original_config_content or "").splitlines()
                ).strip()
            if normalized_original_config: normalized_original_config += "\n"
            else: normalized_original_config = ""


            normalized_finalized_config = "\n".join(
                line.strip() for line in finalized_config_content.splitlines()).strip()
            if normalized_finalized_config:
                normalized_finalized_config += "\n"
            else:
                normalized_finalized_config = ""

            if normalized_finalized_config != normalized_original_config:
                logger.info(f"Webhooks.config for '{repo_name}' requires updates."
                            " Proceeding with update attempt.")
                commit_subject_root = f"Update webhooks.config for {repo_name}" \
                                      f" ({env_type} settings)"
                update_gerrit_repo_webhooks(encoded_repo_name, finalized_config_content,
                                            commit_subject_root)
            else:
                logger.info(f"Webhooks.config for '{repo_name}' sections are already consistent"
                            f" with target {env_type} settings (normalized). No update needed.")
                if encoded_repo_name not in gerrit_repos_with_webhooks_not_updated:
                    gerrit_repos_with_webhooks_not_updated.append(encoded_repo_name)
        else:
            logger.info(f"Webhooks.config for '{repo_name}' sections are already consistent with"
                        f" target {env_type} settings. No update needed.")
            if encoded_repo_name not in gerrit_repos_with_webhooks_not_updated:
                 gerrit_repos_with_webhooks_not_updated.append(encoded_repo_name)
    else:
        if len(args.repos) > 0:
            gerrit_repos = [quote(repo, safe='') for repo in args.repos]
        else:
            gerrit_repos = get_gerrit_repos()

        # Regex to find the section header and capture everything until the next section or EOF
        # DOTALL allows . to match newline, non-greedy .*? ensures we stop at the first \n[ or EOF
        section_re = re.compile(rf"({CONFIG_SECTION_HEADER_ESC}(?:.|\n)*?(?=\n\[|$))", re.DOTALL)
        # Regex to find the URL line within the section
        url_re = re.compile(r"^\s*url\s*=\s*(.*)", re.MULTILINE)

        for repo in gerrit_repos:
            needs_update = False
            new_webhooks_config = None
            webhooks_config = get_gerrit_repo_webhooks(repo)

            if webhooks_config:
                section_match = section_re.search(webhooks_config)
                if section_match:
                    existing_section = section_match.group(1).strip()
                    if args.updateConfig:
                        # Compare entire section with template
                        if existing_section != WEBHOOK_TEMPLATE.strip():
                            logger.info("Config section differs from template for repo %s",
                                        unquote(repo))
                            new_webhooks_config = section_re.sub(WEBHOOK_TEMPLATE, webhooks_config,
                                                                 count=1)
                            needs_update = True
                        else:
                            logger.info("Config section already matches template for repo %s",
                                        unquote(repo))
                            gerrit_repos_with_webhooks_not_updated.append(repo)
                    else:
                        # Only update the URL line within the existing section if
                        # --updateConfig is not set
                        url_match = url_re.search(existing_section)
                        if url_match:
                            existing_url = url_match.group(1).strip()
                            if existing_url != TARGET_URL:
                                logger.info("Webhook URL needs update for repo %s", unquote(repo))
                                # Replace only the URL line within the original config
                                new_url_line = f"  url = {TARGET_URL}"
                                updated_section = url_re.sub(new_url_line,
                                                             existing_section, count=1)
                                new_webhooks_config = webhooks_config.replace(existing_section,
                                                                              updated_section, 1)
                                needs_update = True
                            else:
                                logger.info("Webhook URL already up-to-date for repo %s",
                                            unquote(repo))
                                gerrit_repos_with_webhooks_not_updated.append(repo)
                        else:
                            # Should not happen if section is well-formed, but handle gracefully
                            logger.error("Could not find URL line in existing section for repo %s,"
                                         " and --updateConfig is false.", unquote(repo))
                            gerrit_repos_with_webhooks_to_update_failed.append(repo)
                elif args.add:
                    # Section header not found, but file exists and --add is specified
                    logger.info("Adding webhook section to existing config for repo %s",
                                unquote(repo))
                    # Append template, ensuring a newline separates it from existing content if
                    # webhooks_config is not empty
                    separator = "\n" if webhooks_config.strip() else ""
                    new_webhooks_config = webhooks_config + separator + WEBHOOK_TEMPLATE
                    needs_update = True
                else:
                    # Section header not found, file exists, --add not specified
                    logger.info("No %s section in config for %s and --add not specified.",
                                CONFIG_SECTION_HEADER, unquote(repo))
                    gerrit_repos_with_webhooks_not_updated.append(repo) # Treat as not updated
            elif args.add:
                # webhooks.config file does not exist and --add is specified
                logger.info("Creating webhooks.config for repo %s", unquote(repo))
                new_webhooks_config = WEBHOOK_TEMPLATE
                needs_update = True
            else:
                # webhooks.config file does not exist, --add not specified
                logger.info("No webhooks.config for %s and --add not specified.", unquote(repo))
                gerrit_repos_without_webhooks.append(repo) # Keep track of repos without webhooks

            if needs_update and new_webhooks_config is not None:
                gerrit_repos_with_webhooks.append(repo) # Mark as a candidate for update attempt

                env_mode = 'production' if args.production else 'staging'
                commit_subject: str
                original_config_had_section = False
                if webhooks_config: # webhooks_config is the original content fetched at loop start
                    if section_re.search(webhooks_config):
                        original_config_had_section = True

                if webhooks_config is None: # Implies new file creation (args.add true)
                    commit_subject = (f"Add webhooks.config with qt-cherry-pick-bot section "
                                      f"({env_mode} settings)")
                elif not original_config_had_section and args.add : # Existing file, section added
                     commit_subject = (f"Add qt-cherry-pick-bot section to webhooks.config "
                                       f"({env_mode} settings)")
                else: # Existing file, section existed and is being updated (either URL or full)
                    commit_subject = (f"Update qt-cherry-pick-bot section in webhooks.config "
                                      f"({env_mode} settings)")

                if update_gerrit_repo_webhooks(repo, new_webhooks_config, commit_subject):
                    # This means all steps (create, upload, publish, review, submit) were successful
                    logger.info("Successfully processed (created/updated and submitted) "
                                "webhook config for repo %s", unquote(repo))
                else:
                    # update_gerrit_repo_webhooks handles adding to failed/not_updated lists
                    # and logs specifics. A failure here could mean pre-check found no diff,
                    # or a step in Gerrit interaction failed.
                    logger.info("Webhook config for repo %s was not updated or processing failed." \
                    " See previous logs.", unquote(repo))
            elif needs_update and new_webhooks_config is None:
                 # This case should ideally not be reached if logic for new_webhooks_config is sound
                 logger.error("Internal error: Update flagged but no new config generated for %s.",
                              unquote(repo))
                 gerrit_repos_with_webhooks_to_update_failed.append(repo)

    logger.info("Summary:")
    logger.info("Succeeded: %s", str([unquote(repo) for repo in gerrit_repos_with_webhooks_to_update_succeeded]))
    logger.info("Repos with webhooks that failed to update: %s",
                str([unquote(repo) for repo in gerrit_repos_with_webhooks_to_update_failed]))
    logger.info("Repos with webhooks that were not updated: %s",
                str([unquote(repo) for repo in gerrit_repos_with_webhooks_not_updated]))
    logger.info("Repos without webhooks: %s",
                str([unquote(repo) for repo in gerrit_repos_without_webhooks]))
    logger.info("Repos with webhooks that were not updated: %s",
                str([unquote(repo) for repo in gerrit_repos_with_webhooks_not_updated]))


if __name__ == "__main__":
    main()