Эх сурвалжийг харах

scripts: add script to list custom tags from whitelisted repos

Shiv Tyagi 1 жил өмнө
parent
commit
10b926ff88

+ 259 - 0
scripts/fetch_whitelisted_tags.py

@@ -0,0 +1,259 @@
+"""
+This script automates the listing of tagged versions of
+the ArduPilot source code from various white-listed forks 
+from GitHub on the Custom Build Server.
+It performs the following tasks:
+
+1. **Fetch Tags**: Utilizes the GitHub API to retrieve the list of tags from
+   the specified remote repository.
+2. **Filter Tags**: Identifies tags that start with the prefix 'custom-build/'.
+3. **Update remotes.json**: Adds information about these custom-build tags to
+   the `remotes.json` file, enabling building these versions.
+
+Tag Naming Conventions:
+- Tags named in the format `custom-build/xyz` are allowed to be built
+  for all vehicles.
+- Tags in the format `custom-build/vehicle/xyz` are allowed to be built
+  for the specified vehicle only.
+
+Examples:
+- A tag named `custom-build/some-test-work` will be listed under all
+  the vehicles.
+- A tag named `custom-build/Copter/some-test-work` will be listed under
+  Copter only.
+
+Note:
+- Vehicle names are case-sensitive. e.g., copter is not Copter.
+"""
+import json
+import optparse
+import os
+import requests
+import collections
+
+
+# TODO: move this to base/configs/whitelisted_custom_tag_remotes.json
+remotes = [
+    "ardupilot",
+    "tridge",
+    "peterbarker",
+    "rmackay9",
+    "shiv-tyagi"
+]
+
+vehicles = ['Copter', 'Plane', 'Rover', 'Sub', 'Tracker', 'Blimp', 'Heli']
+
+
+def fetch_tags_from_github(remote):
+    """Returns a list of dictionaries with tag details (name and commit SHA)
+    for the ardupilot repo in the specified remote using the GitHub API
+    See https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28
+    for more details
+    """
+    url = f'https://api.github.com/repos/{remote}/ardupilot/git/refs/tags'
+    headers = {
+        'X-GitHub-Api-Version': '2022-11-28',
+        'Accept': 'application/vnd.github+json'
+    }
+    response = requests.get(url=url, headers=headers)
+    if response.status_code != 200:
+        print(response.text)
+        print(url)
+        raise Exception(
+            "Couldn't fetch tags from github server. "
+            f"Got status code {response.status_code}"
+        )
+
+    tags_objs = response.json()
+    return tags_objs
+
+
+def construct_versions_map(remotes, vehicles):
+    """
+    Returns a dictionary containing the list of versions
+    allowed to be built for different remotes and vehicles.
+
+    The returned dictionary is structured as follows:
+    - The first-level keys represent the remote names.
+    - Each first-level key maps to a nested dictionary.
+    - The nested dictionary has keys representing vehicle names.
+    - Each vehicle name key maps to a list of dictionaries
+    containing the information about the versions allowed
+    to be built for that particular remote and vehicle.
+    - Refer remotes.schema.json to see the schema of the
+    dictionaries about the version information.
+
+    Example:
+    {
+        'remote1': {
+            'vehicleA': [{...}, {...}],
+            'vehicleB': [{...}, {...}, {...}]
+        },
+        'remote2': {
+            'vehicleA': [{...}, {...}],
+            'vehicleB': [{...}, {...}, {...}]
+            'vehicleC': [{...}, {...}],
+        }
+    }
+    """
+
+    ret = collections.defaultdict(
+        lambda: collections.defaultdict(
+            list
+        )
+    )
+
+    for remote in remotes:
+        try:
+            # fetch tag info for ardupilot repo in the remote from github
+            tag_objs = fetch_tags_from_github(remote)
+        except Exception as e:
+            print(e)
+            print("Skipping this remote...")
+            continue
+
+        for tag_info in tag_objs:
+            ref = tag_info['ref']
+            ref = ref.replace('refs/tags/', '')
+
+            s = ref.split('/', 3)
+
+            # skip if tag name does not start with custom-build/
+            if s[0] != 'custom-build':
+                continue
+
+            vehicles_for_tag = []
+            if len(s) == 2:
+                # tag is in the format custom-build/xyz
+                # no vehicle is specified in tag name
+                # list the tag under all vehicles
+                vehicles_for_tag = vehicles
+            else:
+                if s[1] in vehicles:
+                    # tag is in the format custom-build/vehicle/xyz
+                    # list the tag only under this vehicle
+                    vehicles_for_tag = [s[1],]
+
+            for vehicle in vehicles_for_tag:
+                # append an entry to the list of versions listed to be built
+                # for the remote and vehicle
+                ret[remote][vehicle].append(
+                    {
+                        'release_type': f'tag {s[-1]}',
+                        'version_number': 'unreleased',
+                        'ap_build_artifacts_url': f'https://firmware.ardupilot.org/{vehicle}/latest',  # noqa # use master defaults for auto-fetched tags
+                        'commit_reference': tag_info['object']['sha']
+                    }
+                )
+
+    return ret
+
+
+def read_remotes_json_file(path):
+    """Returns python object constructed from the contents of
+    remotes.json file
+
+    If the file cannot be read, it returns an empty list
+    """
+    try:
+        with open(path, 'r') as f:
+            remotes = json.loads(f.read())
+    except Exception as e:
+        print(e)
+        print("Returning empty list")
+        remotes = []
+
+    return remotes
+
+
+def write_remotes_json_file(path, remotes_json_obj):
+    """Serialize the remotes_json_obj object and
+    write to the remotes.json file
+    """
+    with open(path, 'w') as f:
+        f.write(json.dumps(remotes_json_obj, indent=2))
+        print(f"Wrote {path}")
+
+
+def update_remotes_json(path, new_versions_map):
+    """Update remotes.json with the versions listed in new_versions_map
+    """
+    remotes_json_obj = read_remotes_json_file(path)
+
+    # create a dict remote names mapped to the objects containing the
+    # information about that remote in remotes_json_obj to avoid
+    # iterating over the list to access the object every time
+    rname_obj_map = {
+        remote['name']: remote for remote in remotes_json_obj
+    }
+
+    # create another dict with remote names mapped to a dict.
+    # In the secondary dict, the vehicle names are mapped
+    # to the objects in the remotes_json_obj containing
+    # the information about the vehicles listed under the remote
+    rname_vname_obj_map = {
+        remote['name']: {
+            vehicle['name']: vehicle for vehicle in remote['vehicles']
+        } for remote in remotes_json_obj
+    }
+
+    for remote_name, vehicles_obj_dict in new_versions_map.items():
+        # we do not have the remote listed in existing remotes_json_obj
+        if not rname_obj_map.get(remote_name):
+            # create object containing the information about the remote
+            remote_obj = {
+                'name': remote_name,
+                'url': f'https://github.com/{remote_name}/ardupilot.git',
+                'vehicles': []
+            }
+
+            # append to the remotes_json_obj
+            remotes_json_obj.append(remote_obj)
+            # add a reference to the object in rname_obj_map
+            rname_obj_map[remote_name] = remote_obj
+            # no vehicles are listed for this remote yet,
+            # we populate them as we see them ahead
+            rname_vname_obj_map[remote_name] = dict()
+
+        for vehicle_name, versions in vehicles_obj_dict.items():
+            # the vehicle is not listed under this remote
+            if not rname_vname_obj_map[remote_name].get(vehicle_name):
+                # create vehicle object
+                vehicle_obj = {
+                    'name': vehicle_name,
+                    'releases': []
+                }
+
+                # append the vehicle object to the list of vehicle
+                # objects under the remote
+                rname_obj_map[remote_name]['vehicles'].append(vehicle_obj)
+                # add a reference to this object under the remote
+                # and the vehicle name
+                rname_vname_obj_map[remote_name][vehicle_name] = vehicle_obj
+
+            # add the versions listed for this vehicle and
+            # the remote in remotes_json_obj as mentioned above
+            rname_vname_obj_map[remote_name][vehicle_name]['releases'].extend(
+                versions
+            )
+
+    # write the updated obj to the remotes.json file
+    write_remotes_json_file(path, remotes_json_obj)
+    return
+
+
+parser = optparse.OptionParser("fetch_releases.py")
+parser.add_option(
+    "", "--basedir", type="string",
+    default=os.path.abspath(
+        os.path.join(os.path.dirname(__file__), "..", "..", "base")
+    ),
+    help="base directory"
+)
+
+cmd_opts, cmd_args = parser.parse_args()
+basedir = os.path.abspath(cmd_opts.basedir)
+remotes_json_path = os.path.join(basedir, 'configs', 'remotes.json')
+
+new_versions_map = construct_versions_map(remotes, vehicles)
+update_remotes_json(remotes_json_path, new_versions_map)