diff options
author | Daniel Smith <[email protected]> | 2025-06-03 07:36:15 +0000 |
---|---|---|
committer | Daniel Smith <[email protected]> | 2025-06-06 08:02:01 +0000 |
commit | ecdd3bd400efcbf824de03be5a6a3f2233d1c6b6 (patch) | |
tree | fcb123844d8d3072f7e32e3fee84990c513d275b /tools | |
parent | 740e826acf19bc48c1d4b94cb1a40956f1ac17a4 (diff) |
This change updates the webhook installer to
appropriately compare file content before
creating a gerrit change, and ensures that updates
are made via replacement of the existing contents
rather than overwriting the file with the template
directly.
Change-Id: Ia5ccf201b724326069e7627f1f8838d4b3579ec3
Reviewed-by: Daniel Smith <[email protected]>
Diffstat (limited to 'tools')
-rw-r--r-- | tools/webhook_installer/installer.py | 267 |
1 files changed, 201 insertions, 66 deletions
diff --git a/tools/webhook_installer/installer.py b/tools/webhook_installer/installer.py index 9cd74c6..7425ab7 100644 --- a/tools/webhook_installer/installer.py +++ b/tools/webhook_installer/installer.py @@ -32,8 +32,8 @@ parser.add_argument("--add", action="https://code.qt.io/store_true", help="Add webhooks.config to r " have it. Ignored if --repos is not specified") parser.add_argument("--production", action="https://code.qt.io/store_true", help="Update webhooks.config for" " production, defaults to staging if not set") -parser.add_argument("--update-rootconfig", action="https://code.qt.io/store_true", help="Update webhooks.config for the" - " 'All-Projects' repository with specific jira-closer-bot and " +parser.add_argument("--update-rootconfig", action="https://code.qt.io/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="https://code.qt.io/store_true", help="Update the entire" " qt-cherry-pick-bot section if it differs from the template") @@ -58,25 +58,22 @@ gerrit_repos_without_webhooks = [] gerrit_repos_with_webhooks_not_updated = [] -# Configuration for "All-Projects" repository when --update-rootconfig is used -ROOT_CONFIG_ALL_PROJECTS_PROD = """[remote "jira-closer-bot"] - url = https://qt-cherry-pick-bot.herokuapp.com/jiracloser +# Templates for specific sections in "All-Projects" webhooks.config +JIRA_CLOSER_BOT_TEMPLATE = """[remote "jira-closer-bot"] + url = {} maxTries = 10 event = change-merged +""" -[remote "gerrit-webhooks-forwarder"] +GERRIT_WEBHOOKS_FORWARDER_PROD_TEMPLATE = """[remote "gerrit-webhooks-forwarder"] url = http://10.212.0.77:8080 - maxTries = 10""" - -ROOT_CONFIG_ALL_PROJECTS_STAGING = """[remote "jira-closer-bot"] - url = https://qt-cherry-pick-bot-staging.herokuapp.com/jiracloser maxTries = 10 - event = change-merged +""" -# [remote "gerrit-webhooks-forwarder"] +GERRIT_WEBHOOKS_FORWARDER_STAGING_TEMPLATE = """# [remote "gerrit-webhooks-forwarder"] # url = http://10.212.0.77:8080 -# maxTries = 10""" - +# maxTries = 10 +""" CONFIG_SECTION_HEADER = '[remote "qt-cherry-pick-bot"]' CONFIG_SECTION_HEADER_ESC = r'\[remote "qt-cherry-pick-bot"\]' # Escaped for regex @@ -172,20 +169,20 @@ def get_gerrit_repo_webhooks(repo): sys.exit(1) -def update_gerrit_repo_webhooks(repo, webhooks_config): - """ Create a new change request with the webhooks.config file as the only file to be changed. - The change request will be created with the following commit message: - "Update webhook url for qt-cherry-pick-bot to [staging|production]" +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": "Update webhook url for qt-cherry-pick-bot to " - f"{'production' if args.production else 'staging'}", + "subject": commit_subject, "is_work_in_progress": False } if args.simulate: @@ -206,23 +203,30 @@ def update_gerrit_repo_webhooks(repo, webhooks_config): 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", - urllib.parse.unquote(repo)) - logger.error("%s: %s", response.status, response.read().decode("utf-8")) + 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("Failed to connect to gerrit: %s", err.reason) - sys.exit(1) + 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", webhooks_config) + 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 = webhooks_config.encode("utf-8") + 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") @@ -231,22 +235,37 @@ def update_gerrit_repo_webhooks(repo, webhooks_config): 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: - logger.info("Webhooks.config is already up-to-date for %s", - urllib.parse.unquote(repo)) - gerrit_repos_with_webhooks_not_updated.append(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", change_request_id, urllib.parse.unquote(repo)) - logger.error("%s: %s", response.status, response.read().decode("utf-8")) + 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: %s", err.reason) - sys.exit(1) + 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 + # 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)) @@ -327,18 +346,105 @@ def main(): if args.update_rootconfig: repo_name = "All-Projects" encoded_repo_name = quote(repo_name, safe='') - config_content_root: str - env_type: str + + 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: - config_content_root = ROOT_CONFIG_ALL_PROJECTS_PROD + 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: - config_content_root = ROOT_CONFIG_ALL_PROJECTS_STAGING + 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.") - update_gerrit_repo_webhooks(encoded_repo_name, config_content_root) + 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] @@ -363,14 +469,18 @@ def main(): 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) + 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)) + 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 + # 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() @@ -378,25 +488,26 @@ def main(): 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}" - # We need to be careful to replace the URL only within the matched section - # One way is to replace within existing_section and then replace existing_section in webhooks_config - updated_section = url_re.sub(new_url_line, existing_section, count=1) - new_webhooks_config = webhooks_config.replace(existing_section, updated_section, 1) + 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)) + logger.info("Webhook URL already up-to-date for repo %s", + unquote(repo)) gerrit_repos_with_webhooks_not_updated.append(repo) else: - # This case implies the section exists but has no URL, which is unlikely for this template - # but if it happens, and --updateConfig is false, we might want to add the URL or log an error. - # For now, let's assume if --updateConfig is false, we only modify existing URLs. - # If --updateConfig was true, the whole section would be replaced. - logger.error("Could not find URL line in existing section for repo %s, and --updateConfig is false.", unquote(repo)) + # 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 + 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 @@ -416,15 +527,39 @@ def main(): 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 attempted - if update_gerrit_repo_webhooks(repo, new_webhooks_config): - logger.info("Successfully updated/added webhook config for repo %s", unquote(repo)) + 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 logs errors and adds to failed list - logger.error("Failed to update/add webhook config for repo %s", unquote(repo)) + # 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: - # Should not happen if logic is correct, but good to catch - logger.error("Internal error: Update flagged but no new config generated for %s", unquote(repo)) + # 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:") |