diff mbox series

[layerindex-web,2/4] rrs: tools: add create_release.py

Message ID 20260211195536.10278-2-tim.orling@konsulko.com
State New
Headers show
Series [layerindex-web,1/4] requirements: bump to fix vulnerabilities | expand

Commit Message

Tim Orling Feb. 11, 2026, 7:55 p.m. UTC
Allow for a new release and milestones to be created from a YAML file input.
Add PyYAML==6.0.3 to requirements.txt

Fixes: [YOCTO #15578]

Signed-off-by: Tim Orling <tim.orling@konsulko.com>
---
 requirements.txt            |   1 +
 rrs/tools/create_release.py | 160 ++++++++++++++++++++++++++++++++++++
 2 files changed, 161 insertions(+)
 create mode 100755 rrs/tools/create_release.py
diff mbox series

Patch

diff --git a/requirements.txt b/requirements.txt
index 7a3806c..767d4bc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -38,3 +38,4 @@  typing_extensions==4.12.2
 tzdata==2024.1
 vine==5.1.0
 wcwidth==0.2.13
+PyYAML==6.0.3
diff --git a/rrs/tools/create_release.py b/rrs/tools/create_release.py
new file mode 100755
index 0000000..3f14820
--- /dev/null
+++ b/rrs/tools/create_release.py
@@ -0,0 +1,160 @@ 
+#!/usr/bin/env python3
+
+#
+# Create a new release (and its milestones) from a YAML file.
+#
+# Licensed under the MIT license, see COPYING.MIT for details
+#
+# SPDX-License-Identifier: MIT
+
+import sys
+import os.path
+
+sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
+sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', 'layerindex')))
+
+import argparse
+import utils
+import logging
+import yaml
+
+logger = utils.logger_create('CreateRelease')
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Create a new release and milestones from a YAML file",
+        epilog="""Example YAML file:
+
+  release:
+    plan: "OE-Core"
+    name: 5.1
+    start_date: 2024-04-30
+    end_date: 2024-10-25
+    milestones:
+      - name: M1
+        start_date: 2024-04-30
+        end_date: 2024-05-31
+      - name: M2
+        start_date: 2024-06-03
+        end_date: 2024-07-19
+""",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument("yamlfile", help="Path to YAML file defining the release")
+    parser.add_argument('-d', '--debug', action='store_true',
+                        help='Enable debug output')
+    parser.add_argument('-q', '--quiet', action='store_true',
+                        help='Hide all output except error messages')
+    parser.add_argument('-n', '--dry-run', action='store_true',
+                        help='Do not write any changes')
+    parser.add_argument('--update', action='store_true',
+                        help='Update existing release/milestones if they already exist')
+
+    args = parser.parse_args()
+
+    if args.debug:
+        loglevel = logging.DEBUG
+    elif args.quiet:
+        loglevel = logging.WARNING
+    else:
+        loglevel = logging.INFO
+
+    logger.setLevel(loglevel)
+
+    with open(args.yamlfile, 'r') as f:
+        data = yaml.safe_load(f)
+
+    if 'release' not in data:
+        logger.error("YAML file must contain a top-level 'release' key")
+        sys.exit(1)
+
+    reldata = data['release']
+
+    for field in ('plan', 'name', 'start_date', 'end_date'):
+        if field not in reldata:
+            logger.error("Release is missing required field '%s'" % field)
+            sys.exit(1)
+
+    milestones = reldata.get('milestones', [])
+    for i, ms in enumerate(milestones):
+        for field in ('name', 'start_date', 'end_date'):
+            if field not in ms:
+                logger.error("Milestone %d is missing required field '%s'"
+                             % (i + 1, field))
+                sys.exit(1)
+
+    utils.setup_django()
+    from rrs.models import MaintenancePlan, Release, Milestone
+    from django.db import transaction
+
+    plan_name = str(reldata['plan'])
+    maintplan = MaintenancePlan.objects.filter(name=plan_name).first()
+    if not maintplan:
+        logger.error("Maintenance plan '%s' does not exist. Available plans: %s"
+                     % (plan_name,
+                        ', '.join(MaintenancePlan.objects.values_list('name', flat=True))))
+        sys.exit(1)
+
+    release_name = str(reldata['name'])
+
+    try:
+        with transaction.atomic():
+            existing = Release.objects.filter(plan=maintplan,
+                                             name=release_name).first()
+            if existing:
+                if not args.update:
+                    logger.error("Release '%s' already exists for plan '%s' "
+                                 "(use --update to update it)" % (release_name, plan_name))
+                    sys.exit(1)
+                release = existing
+                release.start_date = reldata['start_date']
+                release.end_date = reldata['end_date']
+                release.save()
+                logger.info("Updated release '%s'" % release)
+            else:
+                release = Release.objects.create(
+                    plan=maintplan,
+                    name=release_name,
+                    start_date=reldata['start_date'],
+                    end_date=reldata['end_date'],
+                )
+                logger.info("Created release '%s'" % release)
+
+            for ms in milestones:
+                ms_name = str(ms['name'])
+                existing_ms = Milestone.objects.filter(release=release,
+                                                      name=ms_name).first()
+                if existing_ms:
+                    if not args.update:
+                        logger.error("Milestone '%s' already exists for release "
+                                     "'%s' (use --update to update it)"
+                                     % (ms_name, release))
+                        sys.exit(1)
+                    existing_ms.start_date = ms['start_date']
+                    existing_ms.end_date = ms['end_date']
+                    existing_ms.save()
+                    logger.info("Updated milestone '%s'" % existing_ms)
+                else:
+                    milestone = Milestone.objects.create(
+                        release=release,
+                        name=ms_name,
+                        start_date=ms['start_date'],
+                        end_date=ms['end_date'],
+                    )
+                    logger.info("Created milestone '%s'" % milestone)
+
+            if args.dry_run:
+                raise DryRunRollbackException
+    except DryRunRollbackException:
+        logger.info("Dry run; changes not saved")
+
+    sys.exit(0)
+
+
+class DryRunRollbackException(Exception):
+    pass
+
+
+if __name__ == "__main__":
+    main()