#!/usr/bin/env python3 from __future__ import annotations import sys import glob import subprocess import os import re DRY_RUN = False JSON_SCHEMA_LABEL = "json-schema" # note: we can't use "breaking-change" as the label since that might be applied manually by a user. This is a # distinct label that we can use to indicate that the label was applied (or removed) by automation. BREAKING_CHANGE_LABEL = "detected-breaking-change" def main(changed_files: str | None = None, merge_base_schema_files: str | None = None): global DRY_RUN pr_number = os.environ.get("GITHUB_PR_NUMBER") comment_file_path = os.environ.get("CI_COMMENT_FILE") if not comment_file_path: print("CI_COMMENT_FILE not set") sys.exit(1) if not pr_number: DRY_RUN = True if changed_files: DRY_RUN = True # read lines from file... this is useful for local testing with open(changed_files) as f: pr_changed_files = f.read().splitlines() with open(merge_base_schema_files) as f: og_json_schema_files = sort_json_schema_files(f.read().splitlines()) else: if not is_ci(): print("Not in CI") sys.exit(1) if not pr_number: print("Not a PR") sys.exit(1) pr_changed_files = get_pr_changed_files(pr_number) # since we are running this in the context of the pull_request_target, the checkout is the merge base.. # that is the main branch of the original repo, NOT the branch in the forked repo (or branch in the target # repo for non-forked PRs). This means we just need to list the current checkedout files to get a sense of # the changes before a merge. og_json_schema_files = list_json_schema_files() pr_json_schema_files = filter_to_schema_files(pr_changed_files) pr_labels = get_pr_labels(pr_number) # print("schema files in pr: ", summarize_schema_files(pr_json_schema_files)) # print("og schema files: ", summarize_schema_files(og_json_schema_files)) if not og_json_schema_files: print("No schema files found in merge base") sys.exit(1) # pr_json_schema_files = set of PR files are added, removed, and changed files new_schema_files = set(pr_json_schema_files) - set(og_json_schema_files) removed_or_modified_schema_files = set(pr_json_schema_files) - set(new_schema_files) print("new schemas: ", summarize_schema_files(new_schema_files)) print("removed or modified schemas:", summarize_schema_files(removed_or_modified_schema_files)) # if there is a new or modified schema, we should add the "json-schema" label to the PR... if new_schema_files or removed_or_modified_schema_files: print("\nAdding json-schema label...") add_label(pr_number, JSON_SCHEMA_LABEL) else: if JSON_SCHEMA_LABEL in pr_labels: remove_label(pr_number, JSON_SCHEMA_LABEL) # new schema files should be scrutinized, comparing the latest and added versions to see if it's a breaking # change (major version bump). Warn about it on the PR via adding a breaking-change label... if is_breaking_change(new_schema_files, og_json_schema_files[-1]): print("\nBreaking change detected...") add_label(pr_number, BREAKING_CHANGE_LABEL) else: if BREAKING_CHANGE_LABEL in pr_labels: remove_label(pr_number, BREAKING_CHANGE_LABEL) # modifying an existing schema could be a breaking change, we should warn about it on the PR via a comment... # removing schema files should never be allowed, we should warn about it on the PR via a comment... if removed_or_modified_schema_files: print("\nRemoved or modified schema detected...") schemas = sort_json_schema_files(list(removed_or_modified_schema_files)) schemas_str = "\n".join([f" - {schema}" for schema in schemas]) add_comment(comment_file_path, f"Detected modification or removal of existing json schemas:\n{schemas_str}", warning=True) def add_comment(comment_file_path: str, comment: str, warning: bool = False, important: bool = False): if warning or important: comment_lines = comment.splitlines() comment = "\n".join([f"> {line}" for line in comment_lines]) if warning: comment = f"> [!WARNING]\n{comment}" elif important: comment = f"> [!IMPORTANT]\n{comment}" # create any parent directories if they don't exist os.makedirs(os.path.dirname(comment_file_path), exist_ok=True) with open(comment_file_path, "w") as f: f.write(comment) print(f"Comment file contents: {comment_file_path}") print(comment) def add_label(pr_number: str, label: str): # run "gh pr edit --add-label