nss-release-helper.py (33948B)
1 #!/usr/bin/env python3 2 # This Source Code Form is subject to the terms of the Mozilla Public 3 # License, v. 2.0. If a copy of the MPL was not distributed with this 4 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 6 import os 7 import sys 8 import shutil 9 import re 10 import tempfile 11 from optparse import OptionParser 12 from subprocess import check_call 13 from subprocess import check_output 14 15 nssutil_h = "lib/util/nssutil.h" 16 softkver_h = "lib/softoken/softkver.h" 17 nss_h = "lib/nss/nss.h" 18 nssckbi_h = "lib/ckfw/builtins/nssckbi.h" 19 abi_base_version_file = "automation/abi-check/previous-nss-release" 20 21 abi_report_files = ['automation/abi-check/expected-report-libfreebl3.so.txt', 22 'automation/abi-check/expected-report-libfreeblpriv3.so.txt', 23 'automation/abi-check/expected-report-libnspr4.so.txt', 24 'automation/abi-check/expected-report-libnss3.so.txt', 25 'automation/abi-check/expected-report-libnssckbi.so.txt', 26 'automation/abi-check/expected-report-libnssdbm3.so.txt', 27 'automation/abi-check/expected-report-libnsssysinit.so.txt', 28 'automation/abi-check/expected-report-libnssutil3.so.txt', 29 'automation/abi-check/expected-report-libplc4.so.txt', 30 'automation/abi-check/expected-report-libplds4.so.txt', 31 'automation/abi-check/expected-report-libsmime3.so.txt', 32 'automation/abi-check/expected-report-libsoftokn3.so.txt', 33 'automation/abi-check/expected-report-libssl3.so.txt'] 34 35 36 def check_call_noisy(cmd, *args, **kwargs): 37 print("Executing command: {}".format(cmd)) 38 check_call(cmd, *args, **kwargs) 39 40 41 def print_separator(): 42 print("=" * 70) 43 44 45 def exit_with_failure(what): 46 print("failure: {}".format(what)) 47 sys.exit(2) 48 49 50 def check_files_exist(): 51 if (not os.path.exists(nssutil_h) or not os.path.exists(softkver_h) 52 or not os.path.exists(nss_h) or not os.path.exists(nssckbi_h)): 53 exit_with_failure("cannot find expected header files, must run from inside NSS hg directory") 54 55 56 class Replacement(): 57 def __init__(self, regex="", repl=""): 58 self.regex = regex 59 self.repl = repl 60 self.matcher = re.compile(self.regex) 61 62 def replace(self, line): 63 return self.matcher.sub(self.repl, line) 64 65 66 def inplace_replace(replacements=[], filename=""): 67 for r in replacements: 68 if not isinstance(r, Replacement): 69 raise TypeError("Expecting a list of Replacement objects") 70 71 with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp_file: 72 with open(filename) as in_file: 73 for line in in_file: 74 for r in replacements: 75 line = r.replace(line) 76 tmp_file.write(line) 77 tmp_file.flush() 78 79 shutil.copystat(filename, tmp_file.name) 80 shutil.move(tmp_file.name, filename) 81 os.utime(filename, None) 82 83 84 def toggle_beta_status(is_beta): 85 check_files_exist() 86 if (is_beta): 87 print("adding Beta status to version numbers") 88 inplace_replace(filename=nssutil_h, replacements=[ 89 Replacement(regex=r'^(#define *NSSUTIL_VERSION *\"[0-9.]+)\" *$', 90 repl=r'\g<1> Beta"'), 91 Replacement(regex=r'^(#define *NSSUTIL_BETA *)PR_FALSE *$', 92 repl=r'\g<1>PR_TRUE')]) 93 inplace_replace(filename=softkver_h, replacements=[ 94 Replacement(regex=r'^(#define *SOFTOKEN_VERSION *\"[0-9.]+\" *SOFTOKEN_ECC_STRING) *$', 95 repl=r'\g<1> " Beta"'), 96 Replacement(regex=r'^(#define *SOFTOKEN_BETA *)PR_FALSE *$', 97 repl=r'\g<1>PR_TRUE')]) 98 inplace_replace(filename=nss_h, replacements=[ 99 Replacement(regex=r'^(#define *NSS_VERSION *\"[0-9.]+\" *_NSS_CUSTOMIZED) *$', 100 repl=r'\g<1> " Beta"'), 101 Replacement(regex=r'^(#define *NSS_BETA *)PR_FALSE *$', 102 repl=r'\g<1>PR_TRUE')]) 103 else: 104 print("removing Beta status from version numbers") 105 inplace_replace(filename=nssutil_h, replacements=[ 106 Replacement(regex=r'^(#define *NSSUTIL_VERSION *\"[0-9.]+) *Beta\" *$', 107 repl=r'\g<1>"'), 108 Replacement(regex=r'^(#define *NSSUTIL_BETA *)PR_TRUE *$', 109 repl=r'\g<1>PR_FALSE')]) 110 inplace_replace(filename=softkver_h, replacements=[ 111 Replacement(regex=r'^(#define *SOFTOKEN_VERSION *\"[0-9.]+\" *SOFTOKEN_ECC_STRING) *\" *Beta\" *$', 112 repl=r'\g<1>'), 113 Replacement(regex=r'^(#define *SOFTOKEN_BETA *)PR_TRUE *$', 114 repl=r'\g<1>PR_FALSE')]) 115 inplace_replace(filename=nss_h, replacements=[ 116 Replacement(regex=r'^(#define *NSS_VERSION *\"[0-9.]+\" *_NSS_CUSTOMIZED) *\" *Beta\" *$', 117 repl=r'\g<1>'), 118 Replacement(regex=r'^(#define *NSS_BETA *)PR_TRUE *$', 119 repl=r'\g<1>PR_FALSE')]) 120 121 print("please run 'hg stat' and 'hg diff' to verify the files have been verified correctly") 122 123 124 def print_beta_versions(): 125 check_call_noisy(["egrep", "#define *NSSUTIL_VERSION|#define *NSSUTIL_BETA", nssutil_h]) 126 check_call_noisy(["egrep", "#define *SOFTOKEN_VERSION|#define *SOFTOKEN_BETA", softkver_h]) 127 check_call_noisy(["egrep", "#define *NSS_VERSION|#define *NSS_BETA", nss_h]) 128 129 130 def remove_beta_status(): 131 print("--- removing beta flags. Existing versions were:") 132 print_beta_versions() 133 toggle_beta_status(False) 134 print("=" * 70) 135 print("--- finished modifications, new versions are:") 136 print("=" * 70) 137 print_beta_versions() 138 139 140 def set_beta_status(): 141 print("--- adding beta flags. Existing versions were:") 142 print_beta_versions() 143 toggle_beta_status(True) 144 print("--- finished modifications, new versions are:") 145 print_beta_versions() 146 147 148 def print_library_versions(): 149 check_files_exist() 150 check_call_noisy(["egrep", "#define *NSSUTIL_VERSION|#define NSSUTIL_VMAJOR|#define *NSSUTIL_VMINOR|#define *NSSUTIL_VPATCH|#define *NSSUTIL_VBUILD|#define *NSSUTIL_BETA", nssutil_h]) 151 check_call_noisy(["egrep", "#define *SOFTOKEN_VERSION|#define SOFTOKEN_VMAJOR|#define *SOFTOKEN_VMINOR|#define *SOFTOKEN_VPATCH|#define *SOFTOKEN_VBUILD|#define *SOFTOKEN_BETA", softkver_h]) 152 check_call_noisy(["egrep", "#define *NSS_VERSION|#define NSS_VMAJOR|#define *NSS_VMINOR|#define *NSS_VPATCH|#define *NSS_VBUILD|#define *NSS_BETA", nss_h]) 153 154 155 def print_root_ca_version(): 156 check_files_exist() 157 check_call_noisy(["grep", "define *NSS_BUILTINS_LIBRARY_VERSION", nssckbi_h]) 158 159 160 def ensure_arguments_count(args, how_many, usage): 161 if (len(args) != how_many): 162 exit_with_failure("incorrect number of arguments, expected parameters are:\n" + usage) 163 164 165 def set_major_versions(major): 166 for name, file in [["NSSUTIL_VMAJOR", nssutil_h], 167 ["SOFTOKEN_VMAJOR", softkver_h], 168 ["NSS_VMAJOR", nss_h]]: 169 inplace_replace(filename=file, replacements=[ 170 Replacement(regex=r'^(#define *{} ?).*$'.format(name), 171 repl=r'\g<1>{}'.format(major))]) 172 173 174 def set_minor_versions(minor): 175 for name, file in [["NSSUTIL_VMINOR", nssutil_h], 176 ["SOFTOKEN_VMINOR", softkver_h], 177 ["NSS_VMINOR", nss_h]]: 178 inplace_replace(filename=file, replacements=[ 179 Replacement(regex=r'^(#define *{} ?).*$'.format(name), 180 repl=r'\g<1>{}'.format(minor))]) 181 182 183 def set_patch_versions(patch): 184 for name, file in [["NSSUTIL_VPATCH", nssutil_h], 185 ["SOFTOKEN_VPATCH", softkver_h], 186 ["NSS_VPATCH", nss_h]]: 187 inplace_replace(filename=file, replacements=[ 188 Replacement(regex=r'^(#define *{} ?).*$'.format(name), 189 repl=r'\g<1>{}'.format(patch))]) 190 191 192 def set_build_versions(build): 193 for name, file in [["NSSUTIL_VBUILD", nssutil_h], 194 ["SOFTOKEN_VBUILD", softkver_h], 195 ["NSS_VBUILD", nss_h]]: 196 inplace_replace(filename=file, replacements=[ 197 Replacement(regex=r'^(#define *{} ?).*$'.format(name), 198 repl=r'\g<1>{}'.format(build))]) 199 200 201 def set_full_lib_versions(version): 202 for name, file in [["NSSUTIL_VERSION", nssutil_h], 203 ["SOFTOKEN_VERSION", softkver_h], 204 ["NSS_VERSION", nss_h]]: 205 inplace_replace(filename=file, replacements=[ 206 Replacement(regex=r'^(#define *{} *\")([0-9.]+)(.*)$'.format(name), 207 repl=r'\g<1>{}\g<3>'.format(version))]) 208 209 210 def set_root_ca_version(args): 211 ensure_arguments_count(args, 2, "major_version minor_version") 212 major = args[0].strip() 213 minor = args[1].strip() 214 version = major + '.' + minor 215 216 inplace_replace(filename=nssckbi_h, replacements=[ 217 Replacement(regex=r'^(#define *NSS_BUILTINS_LIBRARY_VERSION *\").*$', 218 repl=r'\g<1>{}"'.format(version)), 219 Replacement(regex=r'^(#define *NSS_BUILTINS_LIBRARY_VERSION_MAJOR ?).*$', 220 repl=r'\g<1>{}'.format(major)), 221 Replacement(regex=r'^(#define *NSS_BUILTINS_LIBRARY_VERSION_MINOR ?).*$', 222 repl=r'\g<1>{}'.format(minor))]) 223 224 225 def set_all_lib_versions(version, major, minor, patch, build): 226 grep_major = check_output(['grep', 'define.*NSS_VMAJOR', nss_h]) 227 grep_minor = check_output(['grep', 'define.*NSS_VMINOR', nss_h]) 228 229 old_major = int(grep_major.split()[2]) 230 old_minor = int(grep_minor.split()[2]) 231 232 new_major = int(major) 233 new_minor = int(minor) 234 235 if (old_major < new_major or (old_major == new_major and old_minor < new_minor)): 236 print("You're increasing the minor (or major) version:") 237 print("- erasing ABI comparison expectations") 238 new_branch = "NSS_" + str(old_major) + "_" + str(old_minor) + "_BRANCH" 239 print("- setting reference branch to the branch of the previous version: " + new_branch) 240 with open(abi_base_version_file, "w") as abi_base: 241 abi_base.write("%s\n" % new_branch) 242 for report_file in abi_report_files: 243 with open(report_file, "w") as report_file_handle: 244 report_file_handle.truncate() 245 246 set_full_lib_versions(version) 247 set_major_versions(major) 248 set_minor_versions(minor) 249 set_patch_versions(patch) 250 set_build_versions(build) 251 252 253 def set_version_to_minor_release(args): 254 ensure_arguments_count(args, 2, "major_version minor_version") 255 major = args[0].strip() 256 minor = args[1].strip() 257 version = major + '.' + minor 258 patch = "0" 259 build = "0" 260 set_all_lib_versions(version, major, minor, patch, build) 261 262 263 def set_version_to_patch_release(args): 264 ensure_arguments_count(args, 3, "major_version minor_version patch_release") 265 major = args[0].strip() 266 minor = args[1].strip() 267 patch = args[2].strip() 268 version = major + '.' + minor + '.' + patch 269 build = "0" 270 set_all_lib_versions(version, major, minor, patch, build) 271 272 273 def set_release_candidate_number(args): 274 ensure_arguments_count(args, 1, "release_candidate_number") 275 build = args[0].strip() 276 set_build_versions(build) 277 278 279 def set_4_digit_release_number(args): 280 ensure_arguments_count(args, 4, "major_version minor_version patch_release 4th_digit_release_number") 281 major = args[0].strip() 282 minor = args[1].strip() 283 patch = args[2].strip() 284 build = args[3].strip() 285 version = major + '.' + minor + '.' + patch + '.' + build 286 set_all_lib_versions(version, major, minor, patch, build) 287 288 289 def make_release_branch(args): 290 ensure_arguments_count(args, 2, "version_string remote") 291 version_string = args[0].strip() 292 remote = args[1].strip() 293 294 major, minor, patch = parse_version_string(version_string) 295 if patch is not None: 296 exit_with_failure("make_release_branch expects a minor version (e.g., '3.117'), not a patch version.") 297 298 version = f"{major}.{minor}" 299 branch_name = f"NSS_{major}_{minor}_BRANCH" 300 tag_name = f"NSS_{major}_{minor}_BETA1" 301 302 print_separator() 303 print("MAKE RELEASE BRANCH") 304 print_separator() 305 print(f"Version: {version}") 306 print(f"Remote: {remote}") 307 print_separator() 308 309 response = input('Are these parameters correct? [yN]: ') 310 if 'y' not in response.lower(): 311 print("Aborted.") 312 sys.exit(0) 313 print_separator() 314 315 # Step 1: Update local repo 316 print("Step 1: Updating local repository...") 317 check_call_noisy(["hg", "pull"]) 318 check_call_noisy(["hg", "checkout", "default"]) 319 print_separator() 320 321 print("Step 2: Checking working directory is clean") 322 hg_status = check_output(["hg", "status"]).decode('utf-8').strip() 323 if hg_status: 324 print() 325 print("ERROR: Working directory is not clean") 326 print(hg_status) 327 print() 328 exit_with_failure("Please commit or revert changes then run this command again. You can reset your working directory with 'hg update -C' and 'hg purge if you want to discard all local changes.") 329 330 branches = check_output(["hg", "branches"]).decode('utf-8').strip() 331 if branch_name in branches: 332 exit_with_failure(f"Branch {branch_name} already exists.") 333 print_separator() 334 335 # Step 2: Verify version numbers are correct 336 print("Step 2: Verifying version numbers are correct...") 337 set_version_to_minor_release([major, minor]) 338 print("=" * 70) 339 set_beta_status() 340 print("=" * 70) 341 # Check if there are any uncommitted changes 342 hg_status = check_output(["hg", "status"]).decode('utf-8').strip() 343 if hg_status: 344 print() 345 print("ERROR: Version numbers are not correctly set") 346 print() 347 print() 348 exit_with_failure("Please check the correct version to freeze, or update the version numbers then run this command again.") 349 350 print("Version numbers verified - no changes needed.") 351 print_separator() 352 353 # Step 3: Create branch 354 print(f"Step 3: Creating branch {branch_name}...") 355 check_call_noisy(["hg", "branch", branch_name]) 356 print_separator() 357 358 # Step 4: Create tag 359 print(f"Step 4: Creating tag {tag_name}...") 360 check_call_noisy(["hg", "tag", tag_name]) 361 print_separator() 362 363 # Step 5: Show outgoing changes 364 response = input('Display outgoing changes? [yN]: ') 365 if 'y' in response.lower(): 366 print() 367 check_call_noisy(["hg", "outgoing", "-p", remote]) 368 print_separator() 369 370 # Step 6: Prompt user and push if confirmed 371 response = input('Push this branch and tag to the NSS repository? [yN]: ') 372 if 'y' in response.lower(): 373 print("Pushing branch and tag...") 374 check_call_noisy(["hg", "push", "--new-branch", remote]) 375 print_separator() 376 print("SUCCESS: Branch and tag have been pushed!") 377 print_separator() 378 print() 379 print("NEXT STEPS:") 380 print(f"1. Wait for the changes to sync to Github: https://github.com/nss-dev/nss/tree/{branch_name}") 381 print("2. In your mozilla-unified repository, run:") 382 print(f" ./mach nss-uplift {tag_name}") 383 print() 384 else: 385 print("Branch and tag have NOT been pushed to the repository.") 386 print("The local branch and tag remain in your working directory.") 387 print_separator() 388 389 390 def parse_version_string(version_string): 391 """Parse a version string like '3.117' or '3.117.1' and return (major, minor, patch) 392 393 For versions like '3.117', patch will be None. 394 Returns: tuple of (major, minor, patch) where patch can be None 395 """ 396 parts = version_string.split('.') 397 if len(parts) < 2: 398 exit_with_failure(f"Invalid version string '{version_string}'. Expected format: 'major.minor' or 'major.minor.patch'") 399 400 major = parts[0].strip() 401 minor = parts[1].strip() 402 patch = parts[2].strip() if len(parts) >= 3 else None 403 404 # Validate that they're numbers 405 try: 406 int(major) 407 int(minor) 408 if patch is not None: 409 int(patch) 410 except ValueError: 411 exit_with_failure(f"Invalid version string '{version_string}'. Version components must be numbers.") 412 413 return major, minor, patch 414 415 416 def version_string_to_RTM_tag(version_string): 417 parts = version_string.split('.') 418 return "NSS_" + "_".join(parts) + "_RTM" 419 420 def version_string_to_underscore(version_string): 421 return version_string.replace('.', '_') 422 423 424 def generate_release_note(args): 425 ensure_arguments_count(args, 3, "this_release_version_string revision_or_tag previous_release_version_string ") 426 427 version = args[0].strip() 428 this_tag = args[1].strip() # Typically going to be . 429 version_underscore = version_string_to_underscore(version) 430 prev_tag = version_string_to_RTM_tag(args[2].strip()) 431 432 # Get the NSPR version 433 nspr_version = check_output(['hg', 'cat', '-r', this_tag, 'automation/release/nspr-version.txt']).decode('utf-8').split("\n")[0].strip() 434 435 # Get the current date 436 from datetime import datetime 437 current_date = datetime.now().strftime("%-d %B %Y") 438 439 # Get the list of bugs from hg log 440 # Get log entries between previous tag and current HEAD 441 command = ["hg", "log", "-r", f"{prev_tag}:{this_tag}", "--template", "{desc|firstline}\\n"] 442 log_output = check_output(command).decode('utf-8') 443 444 # Extract bug numbers and descriptions 445 bug_lines = [] 446 for line in reversed(log_output.split('\n')): 447 if 'Bug' in line or 'bug' in line: 448 line = line.strip() 449 line = line.split("r=")[0].strip() 450 451 # Match patterns like "Bug 1234567 Something" and convert to "Bug 1234567 - Something" 452 line = re.sub(r'(Bug\s+\d+)\s+([^-])', r'\1 - \2', line, flags=re.IGNORECASE) 453 454 # Add a full stop at the end if there isn't one 455 if line: 456 line = line.rstrip(',') 457 458 if line and not line.endswith('.'): 459 line = line + '.' 460 461 if line and line not in bug_lines: 462 bug_lines.append(line) 463 464 changes_text = "\n".join([f" - {line}" for line in bug_lines]) 465 466 # Create the release notes content 467 rst_content = f""".. _mozilla_projects_nss_nss_{version_underscore}_release_notes: 468 469 NSS {version} release notes 470 =============================== 471 472 `Introduction <#introduction>`__ 473 -------------------------------- 474 475 .. container:: 476 477 Network Security Services (NSS) {version} was released on *{current_date}**. 478 479 `Distribution Information <#distribution_information>`__ 480 -------------------------------------------------------- 481 482 .. container:: 483 484 The HG tag is NSS_{version_underscore}_RTM. NSS {version} requires NSPR {nspr_version} or newer. 485 486 NSS {version} source distributions are available on ftp.mozilla.org for secure HTTPS download: 487 488 - Source tarballs: 489 https://ftp.mozilla.org/pub/mozilla.org/security/nss/releases/NSS_{version_underscore}_RTM/src/ 490 491 Other releases are available :ref:`mozilla_projects_nss_releases`. 492 493 .. _changes_in_nss_{version}: 494 495 `Changes in NSS {version} <#changes_in_nss_{version}>`__ 496 ------------------------------------------------------------------ 497 498 .. container:: 499 500 {changes_text} 501 502 """ 503 return rst_content 504 505 506 def generate_release_notes_index(args): 507 ensure_arguments_count(args, 2, "latest_release_version latest_esr_version") 508 latest_version = args[0].strip() # e.g. 3.116 509 esr_version = args[1].strip() # e.g. 3.112.1 510 511 latest_underscore = version_string_to_underscore(latest_version) 512 esr_underscore = version_string_to_underscore(esr_version) 513 514 # Read all release note files from doc/rst/releases/ 515 release_dir = "doc/rst/releases" 516 if not os.path.exists(release_dir): 517 exit_with_failure(f"Release notes directory not found: {release_dir}") 518 519 # Get all nss_*.rst files (excluding index.rst) 520 release_files = [] 521 for filename in os.listdir(release_dir): 522 if filename.startswith("nss_") and filename.endswith(".rst") and filename != "index.rst": 523 release_files.append(filename) 524 525 # Sort release files in reverse order (newest first) 526 # Extract version numbers for proper sorting 527 def version_key(filename): 528 # Extract version parts from filename like nss_3_116.rst 529 parts = filename.replace("nss_", "").replace(".rst", "").split("_") 530 # Convert to integers for proper numerical sorting 531 return [int(p) for p in parts] 532 533 release_files.sort(key=version_key, reverse=True) 534 535 # Build the toctree content 536 toctree_lines = "\n".join([f" {f}" for f in release_files]) 537 538 # Create the index.rst content 539 index_content = f""".. _mozilla_projects_nss_releases: 540 541 Release Notes 542 ============= 543 544 .. toctree:: 545 :maxdepth: 0 546 :glob: 547 :hidden: 548 549 {toctree_lines} 550 551 .. note:: 552 553 **NSS {latest_version}** is the latest version of NSS. 554 Complete release notes are available here: :ref:`mozilla_projects_nss_nss_{latest_underscore}_release_notes` 555 556 **NSS {esr_version} (ESR)** is the latest ESR version of NSS. 557 Complete release notes are available here: :ref:`mozilla_projects_nss_nss_{esr_underscore}_release_notes` 558 559 """ 560 561 index_file = os.path.join(release_dir, "index.rst") 562 with open(index_file, "w") as f: 563 f.write(index_content) 564 565 print(f"Generated {index_file}") 566 print() 567 print("=" * 70) 568 print("Content:") 569 print("=" * 70) 570 print(index_content) 571 572 573 def release_nss(args): 574 ensure_arguments_count(args, 4, "version_string previous_version esr_version remote") 575 version_string = args[0].strip() 576 previous_version = args[1].strip() 577 esr_version = args[2].strip() 578 remote = args[3].strip() 579 580 major, minor, patch = parse_version_string(version_string) 581 582 # Build version string and related names 583 version = version_string 584 version_underscore = version_string_to_underscore(version_string) 585 branch_name = f"NSS_{major}_{minor}_BRANCH" 586 rtm_tag = f"NSS_{version_underscore}_RTM" 587 release_note_file = f"doc/rst/releases/nss_{version_underscore}.rst" 588 589 print_separator() 590 print("RELEASE NSS") 591 print_separator() 592 print(f"Release version: {version}") 593 print(f"Previous version: {previous_version}") 594 print(f"ESR version: {esr_version}") 595 print(f"Remote: {remote}") 596 print_separator() 597 598 response = input('Are these parameters correct? [yN]: ') 599 if 'y' not in response.lower(): 600 print("Aborted.") 601 sys.exit(0) 602 print_separator() 603 604 print("=" * 70) 605 print(f"Starting NSS {version} release process") 606 print("=" * 70) 607 print() 608 609 # Step 1: Update local repo 610 print("Step 1: Updating local repository...") 611 check_call_noisy(["hg", "pull"]) 612 print_separator() 613 614 # Step 2: Checking working directory is clean 615 print("Step 2: Checking working directory is clean...") 616 hg_status = check_output(["hg", "status"]).decode('utf-8').strip() 617 if hg_status: 618 print() 619 print("ERROR: Working directory is not clean") 620 print(hg_status) 621 print() 622 exit_with_failure("Please commit or revert changes then run this command again. You can reset your working directory with 'hg update -C' and 'hg purge if you want to discard all local changes.") 623 print_separator() 624 625 # Step 3: Make sure we're on the appropriate branch 626 print(f"Step 3: Checking out branch {branch_name}...") 627 try: 628 check_call_noisy(["hg", "checkout", branch_name]) 629 except Exception as e: 630 exit_with_failure(f"Failed to checkout branch {branch_name}. Does it exist?") 631 print_separator() 632 633 # Step 4: Check for any existing commits or tags 634 print("Step 4: Checking for existing release commits or tags...") 635 636 # Check if RTM tag already exists 637 tags_output = check_output(["hg", "tags"]).decode('utf-8') 638 if rtm_tag in tags_output: 639 exit_with_failure(f"Tag {rtm_tag} already exists. Has this release already been made?") 640 641 # Check for recent commits with the same commit messages we're about to make 642 version_commit_message = f"Set version numbers to {version} final" 643 release_notes_commit_message = f"Release notes for NSS {version}" 644 645 recent_log = check_output(["hg", "log", "-l", "5", "--template", "{desc|firstline}\\n"]).decode('utf-8') 646 647 if version_commit_message in recent_log: 648 exit_with_failure(f"Found recent commit with message '{version_commit_message}'. Has this release already been started?") 649 650 if release_notes_commit_message in recent_log: 651 exit_with_failure(f"Found recent commit with message '{release_notes_commit_message}'. Has this release already been started?") 652 653 print("No existing release commits or tags found.") 654 print_separator() 655 656 # Step 5: Update the NSS version numbers (remove beta) 657 print("Step 5: Removing beta status from version numbers...") 658 if patch: 659 set_version_to_patch_release([major, minor, patch]) 660 else: 661 set_version_to_minor_release([major, minor]) 662 remove_beta_status() 663 664 print_separator() 665 666 667 668 # Step 6: Commit the change 669 print("Step 6: Committing version number changes...") 670 check_call_noisy(["hg", "commit", "-m", version_commit_message]) 671 print_separator() 672 673 # Step 7: Generate release note 674 print("Step 7: Generating release notes...") 675 release_note_content = generate_release_note([version, ".", previous_version]) 676 677 # Write release note to file 678 with open(release_note_file, "w") as f: 679 f.write(release_note_content) 680 print(f"Release note written to {release_note_file}") 681 print_separator() 682 683 # Step 8: Generate new release note index 684 print("Step 8: Generating release notes index...") 685 generate_release_notes_index([version, esr_version]) 686 print_separator() 687 688 input("Are you making an ESR release? If so, please manually edit doc/rst/releases/index.rst to adjust the ESR / main version note. Press enter when done.") 689 690 # Step 9: Commit the release notes 691 print("Step 9: Committing release notes...") 692 check_call_noisy(["hg", "add", release_note_file]) 693 check_call_noisy(["hg", "commit", "-m", release_notes_commit_message]) 694 695 # Get the commit hash 696 docs_commit = check_output(["hg", "log", "-r", ".", "--template", "{node|short}"]).decode('utf-8').strip() 697 print(f"Release notes committed. Commit hash: {docs_commit}") 698 print_separator() 699 700 # Step 10: Tag the release version 701 print(f"Step 10: Tagging release version {rtm_tag}...") 702 check_call_noisy(["hg", "tag", rtm_tag]) 703 print_separator() 704 705 # Step 11: Switch to default branch and graft the release notes 706 print("Step 11: Switching to default branch and grafting release notes...") 707 check_call_noisy(["hg", "checkout", "default"]) 708 check_call_noisy(["hg", "graft", "-r", docs_commit]) 709 print_separator() 710 711 response = input('Display the outgoing changes? [yN]: ') 712 if 'y' in response.lower(): 713 check_call_noisy(["hg", "outgoing", "--graph", "-b", "default", "-b", branch_name, remote]) 714 print_separator() 715 716 # Step 12: Push changes 717 response = input('Push these changes to the NSS repository? [yN]: ') 718 if 'y' in response.lower(): 719 print("Pushing changes to default branch...") 720 check_call_noisy(["hg", "push", "-b", "default", remote]) 721 print(f"Pushing changes to {branch_name} branch...") 722 check_call_noisy(["hg", "push", "-b", branch_name, remote]) 723 print_separator() 724 print("SUCCESS: NSS release process completed!") 725 print_separator() 726 print() 727 print("NEXT STEPS:") 728 print(f"1. Wait for the changes to sync to Github") 729 print("2. In your mozilla-unified repository, run:") 730 print(f" ./mach nss-uplift {rtm_tag}") 731 print() 732 else: 733 print("Changes have NOT been pushed to the repository.") 734 print("The local commits remain in your working directory.") 735 print_separator() 736 737 738 def create_nss_release_archive(args): 739 ensure_arguments_count(args, 2, "nss_release_version path_to_stage_directory") 740 nssrel = args[0].strip() # e.g. 3.19.3 741 stagedir = args[1].strip() # e.g. ../stage 742 743 # Determine which tar command to use (prefer gtar if available) 744 tar_cmd = "gtar" 745 try: 746 check_call(["which", "gtar"], stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w')) 747 except: 748 tar_cmd = "tar" 749 750 # Generate the release tag from the version 751 nssreltag = version_string_to_RTM_tag(nssrel) 752 753 print_separator() 754 print("CREATE NSS RELEASE ARCHIVE") 755 print_separator() 756 print(f"NSS release version: {nssrel}") 757 print(f"Stage directory: {stagedir}") 758 print_separator() 759 760 response = input('Are these parameters correct? [yN]: ') 761 if 'y' not in response.lower(): 762 print("Aborted.") 763 sys.exit(0) 764 print_separator() 765 766 with open('automation/release/nspr-version.txt') as nspr_version_file: 767 nsprrel = next(nspr_version_file).strip() 768 769 nspr_tar = "nspr-" + nsprrel + ".tar.gz" 770 nspr_dir = stagedir + "/v" + nsprrel + "/src/" 771 nsprtar_with_path = nspr_dir + nspr_tar 772 773 nspr_releases_url = "https://ftp.mozilla.org/pub/nspr/releases" 774 if (not os.path.exists(nsprtar_with_path)): 775 os.makedirs(nspr_dir,exist_ok=True) 776 check_call_noisy(['wget', f"{nspr_releases_url}/v{nsprrel}/src/nspr-{nsprrel}.tar.gz", 777 f'--output-document={nsprtar_with_path}']) 778 779 if (not os.path.exists(nsprtar_with_path)): 780 exit_with_failure("cannot find nspr archive at expected location " + nsprtar_with_path) 781 782 nss_stagedir = stagedir + "/" + nssreltag + "/src" 783 if (os.path.exists(nss_stagedir)): 784 exit_with_failure("nss stage directory already exists: " + nss_stagedir) 785 786 nss_tar = "nss-" + nssrel + ".tar.gz" 787 788 check_call_noisy(["mkdir", "-p", nss_stagedir]) 789 check_call_noisy(["hg", "archive", "-r", nssreltag, "--prefix=nss-" + nssrel + "/nss", 790 stagedir + "/" + nssreltag + "/src/" + nss_tar, "-X", ".hgtags"]) 791 check_call_noisy([tar_cmd, "-xz", "-C", nss_stagedir, "-f", nsprtar_with_path]) 792 print("changing to directory " + nss_stagedir) 793 os.chdir(nss_stagedir) 794 check_call_noisy([tar_cmd, "-xz", "-f", nss_tar]) 795 check_call_noisy(["mv", "-i", "nspr-" + nsprrel + "/nspr", "nss-" + nssrel + "/"]) 796 check_call_noisy(["rmdir", "nspr-" + nsprrel]) 797 798 nss_nspr_tar = "nss-" + nssrel + "-with-nspr-" + nsprrel + ".tar.gz" 799 800 check_call_noisy([tar_cmd, "-cz", "-f", nss_nspr_tar, "nss-" + nssrel]) 801 check_call_noisy(["rm", "-rf", "nss-" + nssrel]) 802 check_call("sha1sum " + nss_tar + " " + nss_nspr_tar + " > SHA1SUMS", shell=True) 803 check_call("sha256sum " + nss_tar + " " + nss_nspr_tar + " > SHA256SUMS", shell=True) 804 print("created directory " + nss_stagedir + " with files:") 805 check_call_noisy(["ls", "-l"]) 806 807 if 'y' not in input('Upload release tarball?[yN]'): 808 print("Release tarballs have NOT been uploaded") 809 exit(0) 810 os.chdir("../..") 811 gcp_proj="moz-fx-productdelivery-pr-38b5" 812 check_call_noisy(["gcloud", "auth", "login"]) 813 check_call_noisy( 814 [ 815 "gcloud", 816 "--project", 817 gcp_proj, 818 f"--impersonate-service-account=nss-team-prod@{gcp_proj}.iam.gserviceaccount.com", 819 "storage", 820 "cp", 821 "--recursive", 822 "--no-clobber", 823 nssreltag, 824 f"gs://{gcp_proj}-productdelivery/pub/security/nss/releases/", 825 ] 826 ) 827 print_separator() 828 print(f"Release tarballs have been uploaded to Google Cloud Storage. You can find them at https://ftp.mozilla.org/pub/security/nss/releases/{nssreltag}/") 829 print_separator() 830 831 832 o = OptionParser(usage="client.py [options] " + " | ".join([ 833 "remove_beta", "set_beta", "print_library_versions", "print_root_ca_version", 834 "set_root_ca_version", "set_version_to_minor_release", 835 "set_version_to_patch_release", "set_release_candidate_number", 836 "set_4_digit_release_number", "make_release_branch", "create_nss_release_archive", 837 "generate_release_note", "generate_release_notes_index"])) 838 839 try: 840 options, args = o.parse_args() 841 action = args[0] 842 action_args = args[1:] # Get all arguments after the action 843 except IndexError: 844 o.print_help() 845 sys.exit(2) 846 847 if action in ('remove_beta'): 848 remove_beta_status() 849 850 elif action in ('set_beta'): 851 set_beta_status() 852 853 elif action in ('print_library_versions'): 854 print_library_versions() 855 856 elif action in ('print_root_ca_version'): 857 print_root_ca_version() 858 859 elif action in ('set_root_ca_version'): 860 set_root_ca_version(action_args) 861 862 # x.y version number - 2 parameters 863 elif action in ('set_version_to_minor_release'): 864 set_version_to_minor_release(action_args) 865 866 # x.y.z version number - 3 parameters 867 elif action in ('set_version_to_patch_release'): 868 set_version_to_patch_release(action_args) 869 870 # change the release candidate number, usually increased by one, 871 # usually if previous release candiate had a bug 872 # 1 parameter 873 elif action in ('set_release_candidate_number'): 874 set_release_candidate_number(action_args) 875 876 # use the build/release candiate number in the identifying version number 877 # 4 parameters 878 elif action in ('set_4_digit_release_number'): 879 set_4_digit_release_number(action_args) 880 881 # create a freeze branch and beta tag for a new release 882 # 2 parameters 883 elif action in ('make_release_branch'): 884 make_release_branch(action_args) 885 886 elif action in ('create_nss_release_archive'): 887 create_nss_release_archive(action_args) 888 889 elif action in ('generate_release_note'): 890 print(generate_release_note(action_args)) 891 892 elif action in ('generate_release_notes_index'): 893 generate_release_notes_index(action_args) 894 895 896 elif action in ('release_nss'): 897 release_nss(action_args) 898 899 else: 900 o.print_help() 901 sys.exit(2) 902 903 sys.exit(0)