tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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)