@@ -792,6 +792,147 @@ def delete_release(
792792 logging .debug (f"Release '{ args .delete } ' will be deleted." )
793793
794794
795+ def update_release (
796+ args ,
797+ next_releases ,
798+ major_releases ,
799+ minor_releases ,
800+ nightly_releases ,
801+ dev_releases ,
802+ ):
803+ """Update an existing release by name, modifying specified fields in-place."""
804+ release_type , major , minor , patch = parse_release_name (args .update )
805+
806+ # Select the appropriate list based on release_type
807+ if release_type == "next" :
808+ release_list = next_releases
809+ elif release_type == "major" :
810+ release_list = major_releases
811+ elif release_type == "minor" :
812+ release_list = minor_releases
813+ elif release_type == "nightly" :
814+ release_list = nightly_releases
815+ elif release_type == "dev" :
816+ release_list = dev_releases
817+ else :
818+ logging .error (f"Error: Unknown release type '{ release_type } ' in release name." )
819+ sys .exit (ERROR_CODES ["validation_error" ])
820+
821+ # Validate field applicability for the release type
822+ if args .lifecycle_extended_isodatetime and release_type not in ["major" , "next" ]:
823+ logging .error (
824+ f"Error: '--lifecycle-extended-isodatetime' is only valid for "
825+ f"'major' and 'next' release types, not '{ release_type } '."
826+ )
827+ sys .exit (ERROR_CODES ["validation_error" ])
828+
829+ if args .lifecycle_eol_isodatetime and release_type not in [
830+ "next" ,
831+ "major" ,
832+ "minor" ,
833+ ]:
834+ logging .error (
835+ f"Error: '--lifecycle-eol-isodatetime' is only valid for "
836+ f"'next', 'major', and 'minor' release types, not '{ release_type } '."
837+ )
838+ sys .exit (ERROR_CODES ["validation_error" ])
839+
840+ if args .commit and release_type not in ["minor" , "nightly" , "dev" ]:
841+ logging .error (
842+ f"Error: '--commit' is only valid for 'minor', 'nightly', and "
843+ f"'dev' release types, not '{ release_type } '."
844+ )
845+ sys .exit (ERROR_CODES ["validation_error" ])
846+
847+ # Find the release by name
848+ release = None
849+ for r in release_list :
850+ if r ["name" ] == args .update :
851+ release = r
852+ break
853+
854+ if release is None :
855+ logging .error (f"Error: Release '{ args .update } ' not found in the existing data." )
856+ sys .exit (ERROR_CODES ["validation_error" ])
857+
858+ # Apply lifecycle.released update
859+ if args .lifecycle_released_isodatetime :
860+ try :
861+ released_date = datetime .strptime (
862+ args .lifecycle_released_isodatetime , "%Y-%m-%dT%H:%M:%S"
863+ ).replace (tzinfo = pytz .UTC )
864+ except ValueError :
865+ logging .error (
866+ "Error: Invalid --lifecycle-released-isodatetime format. "
867+ "Use ISO format: YYYY-MM-DDTHH:MM:SS"
868+ )
869+ sys .exit (ERROR_CODES ["validation_error" ])
870+ if "released" not in release ["lifecycle" ]:
871+ release ["lifecycle" ]["released" ] = {}
872+ release ["lifecycle" ]["released" ]["isodate" ] = released_date .strftime ("%Y-%m-%d" )
873+ release ["lifecycle" ]["released" ]["timestamp" ] = int (released_date .timestamp ())
874+ logging .info (
875+ f"Updated lifecycle.released for '{ args .update } ' to "
876+ f"{ released_date .strftime ('%Y-%m-%d' )} ."
877+ )
878+
879+ # Apply lifecycle.extended update (only for major/next)
880+ if args .lifecycle_extended_isodatetime :
881+ try :
882+ extended_date = datetime .strptime (
883+ args .lifecycle_extended_isodatetime , "%Y-%m-%dT%H:%M:%S"
884+ ).replace (tzinfo = pytz .UTC )
885+ except ValueError :
886+ logging .error (
887+ "Error: Invalid --lifecycle-extended-isodatetime format. "
888+ "Use ISO format: YYYY-MM-DDTHH:MM:SS"
889+ )
890+ sys .exit (ERROR_CODES ["validation_error" ])
891+ if "extended" not in release ["lifecycle" ]:
892+ release ["lifecycle" ]["extended" ] = {}
893+ release ["lifecycle" ]["extended" ]["isodate" ] = extended_date .strftime ("%Y-%m-%d" )
894+ release ["lifecycle" ]["extended" ]["timestamp" ] = int (extended_date .timestamp ())
895+ logging .info (
896+ f"Updated lifecycle.extended for '{ args .update } ' to "
897+ f"{ extended_date .strftime ('%Y-%m-%d' )} ."
898+ )
899+
900+ # Apply lifecycle.eol update (only for next/major/minor)
901+ if args .lifecycle_eol_isodatetime :
902+ try :
903+ eol_date = datetime .strptime (
904+ args .lifecycle_eol_isodatetime , "%Y-%m-%dT%H:%M:%S"
905+ ).replace (tzinfo = pytz .UTC )
906+ except ValueError :
907+ logging .error (
908+ "Error: Invalid --lifecycle-eol-isodatetime format. "
909+ "Use ISO format: YYYY-MM-DDTHH:MM:SS"
910+ )
911+ sys .exit (ERROR_CODES ["validation_error" ])
912+ if "eol" not in release ["lifecycle" ]:
913+ release ["lifecycle" ]["eol" ] = {}
914+ release ["lifecycle" ]["eol" ]["isodate" ] = eol_date .strftime ("%Y-%m-%d" )
915+ release ["lifecycle" ]["eol" ]["timestamp" ] = int (eol_date .timestamp ())
916+ logging .info (
917+ f"Updated lifecycle.eol for '{ args .update } ' to "
918+ f"{ eol_date .strftime ('%Y-%m-%d' )} ."
919+ )
920+
921+ # Apply commit update (only for minor/nightly/dev)
922+ if args .commit :
923+ commit = args .commit
924+ if len (commit ) != 40 :
925+ logging .error ("Error: Invalid commit hash. Must be 40 characters." )
926+ sys .exit (ERROR_CODES ["validation_error" ])
927+ if "git" not in release :
928+ release ["git" ] = {}
929+ release ["git" ]["commit" ] = commit
930+ release ["git" ]["commit_short" ] = commit [:8 ]
931+ logging .info (f"Updated git.commit for '{ args .update } ' to { commit [:8 ]} ." )
932+
933+ logging .debug (f"Release '{ args .update } ' will be updated." )
934+
935+
795936def merge_input_data (existing_releases , new_releases ):
796937 """Merge two lists of releases, updating existing releases with new releases."""
797938 # Create a dictionary of releases by name from existing_releases
@@ -1369,6 +1510,59 @@ def handle_releases(args):
13691510 dev_releases ,
13701511 )
13711512
1513+ elif args .update :
1514+ if args .no_query :
1515+ logging .error ("Error: '--update' cannot run with '--no-query'." )
1516+ sys .exit (ERROR_CODES ["parameter_missing" ])
1517+ # Validate at least one modifier is provided
1518+ if not any (
1519+ [
1520+ args .lifecycle_released_isodatetime ,
1521+ args .lifecycle_extended_isodatetime ,
1522+ args .lifecycle_eol_isodatetime ,
1523+ args .commit ,
1524+ ]
1525+ ):
1526+ logging .error (
1527+ "Error: '--update' requires at least one of: "
1528+ "--lifecycle-released-isodatetime, --lifecycle-extended-isodatetime, "
1529+ "--lifecycle-eol-isodatetime, --commit."
1530+ )
1531+ sys .exit (ERROR_CODES ["parameter_missing" ])
1532+
1533+ # Add stdin input or file input data if provided (existing releases will be overwritten)
1534+ if args .input_stdin or args .input :
1535+ if args .input_stdin :
1536+ (
1537+ input_next ,
1538+ input_major ,
1539+ input_minor ,
1540+ input_nightly ,
1541+ input_dev ,
1542+ ) = load_input_stdin ()
1543+ elif args .input :
1544+ (
1545+ input_next ,
1546+ input_major ,
1547+ input_minor ,
1548+ input_nightly ,
1549+ input_dev ,
1550+ ) = load_input (args .input_file )
1551+ next_releases = merge_input_data (next_releases , input_next )
1552+ major_releases = merge_input_data (major_releases , input_major )
1553+ minor_releases = merge_input_data (minor_releases , input_minor )
1554+ nightly_releases = merge_input_data (nightly_releases , input_nightly )
1555+ dev_releases = merge_input_data (dev_releases , input_dev )
1556+
1557+ update_release (
1558+ args ,
1559+ next_releases ,
1560+ major_releases ,
1561+ minor_releases ,
1562+ nightly_releases ,
1563+ dev_releases ,
1564+ )
1565+
13721566 else :
13731567 if create_initial_major or create_initial_minor :
13741568 github_releases = get_github_releases ()
@@ -1605,6 +1799,14 @@ def parse_arguments():
16051799 help = "Delete a release by name (format: type-major.minor or "
16061800 "type-major.minor.patch). Requires --s3-update." ,
16071801 )
1802+ parser .add_argument (
1803+ "--update" ,
1804+ type = str ,
1805+ help = "Update an existing release by name (format: type-major.minor or "
1806+ "type-major.minor.patch or type-major). Requires at least one of: "
1807+ "--lifecycle-released-isodatetime, --lifecycle-extended-isodatetime, "
1808+ "--lifecycle-eol-isodatetime, --commit." ,
1809+ )
16081810 parser .add_argument (
16091811 "--create-initial-releases" ,
16101812 type = str ,
0 commit comments