diff --git a/.github/workflows/modify_documentation.py b/.github/workflows/modify_documentation.py new file mode 100644 index 000000000..bb7331a94 --- /dev/null +++ b/.github/workflows/modify_documentation.py @@ -0,0 +1,511 @@ +import yaml +import sys, os +import re +import pathlib + +def load_yaml(file_path): + """Loads a YAML file and returns its content.""" + try: + # Open and read the YAML file + with open(file_path, 'r') as file: + patch_data = yaml.safe_load(file) + print("YAML Version Data loaded successfully.\n\n") + return patch_data + except FileNotFoundError: + # Handle case where the file does not exist + print(f"Error: '{file_path}' not found.\n\n") + sys.exit(1) + except yaml.YAMLError as exc: + # Handle errors during YAML parsing + print(f"Error parsing YAML file: {exc}\n\n") + sys.exit(1) + +def load_md(file_path): + """Loads a Markdown file and returns its content as a list of lines.""" + try: + # Open and read the Markdown file into a list of lines + with open(file_path, 'r') as file: + content = file.readlines() + print("Markdown file loaded successfully.\n\n") + return content + except FileNotFoundError: + # Handle case where the file does not exist + print(f"Error: '{file_path}' not found.\n\n") + sys.exit(1) + +def save_md(file_path, content): + """Saves the given content to a Markdown file.""" + with open(file_path, 'w') as file: + file.write(content) + print("Markdown file saved successfully.\n\n") + +def gi_software_insert_docs(gi_software, overrides, documentation): + """Inserts GI software information into the documentation's HTML table.""" + # Iterate over each GI software entry from the YAML data + for each_patch in gi_software: + version = each_patch['version'] + file_names = [] + # Collect all file names and alternate names + for file in each_patch['files']: + file_names.append(file['name']) + file_names.append(file['alt_name']) + + # Use override values from YAML if they exist, otherwise use defaults + try: + if overrides['category'] != "": + category = overrides['category'] + else: + category = "Base - eDelivery or OTN" + except: + category = "Base - eDelivery or OTN" + + try: + if overrides['software_piece'] != "": + software_piece = overrides['software_piece'] + else: + software_piece = "Oracle Database {0} for Linux x86-64".format(version) + except: + software_piece = "Oracle Database {0} for Linux x86-64".format(version) + + try: + if overrides['file_name'] != "": + files = overrides['file_name'] + else: + files = "{0} or {1}".format(file_names[0], file_names[1]) + except: + files = "{0} or {1}".format(file_names[0], file_names[1]) + + # Format the new HTML table row + table_row = """\n\n{category}\n{software_piece}\n{files}\n\n""". format( + category=category, + software_piece=software_piece, + files=files + ) + table_found = False + section_found = False + # Iterate through the documentation lines to find the insertion point + for idx, line in enumerate(documentation): + if table_found and section_found: + try: + # Check if the line is a table row with a version + if len(line.split(">")) == 3: + version_text = line.split(">")[1].split("<")[0] + # Check if the text is a valid version number + if re.match(r'^[0-9.]+$', version_text): + # Insert the new row before an existing row with a lower version number + if version_text != version and version_text < version: + documentation.insert(idx - 1 , table_row) + print("Inserted row: {0} at line {1} in documentation.\n".format(table_row, idx)) + break + except IndexError: + continue + # Find the target section header + if line.strip() == "#### Required Oracle Software - Download Summary": + section_found = True + # Find the table within the target section + if section_found and line.strip() == "": + table_found = True + + return documentation + +def gi_interim_insert_patch(gi_interim_patches, overrides, documentation): + """Inserts GI interim patch information into the documentation's HTML table.""" + # Iterate over each GI interim patch entry + for each_patch in gi_interim_patches: + version = each_patch['version'] + patchfile = each_patch['files'][0]['name'] + + # Use override values from YAML if they exist, otherwise use defaults + try: + if overrides['category'] != "": + category = overrides['category'] + else: + category = "" + except: + category = "" + + try: + if overrides['software_piece'] != "": + software_piece = overrides['software_piece'] + else: + software_piece = "GI Interim Patch" + except: + software_piece = "GI Interim Patch" + + try: + if overrides['file_name'] != "": + files = overrides['file_name'] + else: + files = patchfile + except: + files = patchfile + + # Format the new HTML table row + table_row = """\n\n\n\n\n\n""". format( + category=category, + software_piece=software_piece, + files=files + ) + table_found = False + section_found = False + base_found = False # This variable is declared but not used + # Iterate through the documentation lines to find the insertion point + for idx, line in enumerate(documentation): + + if section_found and table_found: + try: + # Check if the line is a table row with a version + if len(line.split(">")) == 3: + version_text = line.split(">")[1].split("<")[0] + # Check if the text is a valid version number + if re.match(r'^[0-9.]+$', version_text): + # Insert the new row before an existing row with a lower version number + if version_text != version and version_text < version: + documentation.insert(idx - 1 , table_row) + print("Inserted row: {0} at line {1} in documentation.\n".format(table_row, idx)) + break + except IndexError: + continue + + # Find the target section header + if line.strip() == "#### Required Oracle Software - Download Summary": + section_found = True + + # Find the table body within the target section + if section_found and line.strip() == "": + table_found = True + + # Stop searching once the end of the table body is reached + if table_found and line.strip() == "": + break + + return documentation + +def gi_patches_insert_docs(gi_patches, overrides, documentation): + """Inserts GI patch information into the documentation's HTML table.""" + # Iterate over each GI patch entry + for each_patch in gi_patches: + version = each_patch['release'] + patchfile = each_patch['patchfile'] + + # Use override values from YAML if they exist, otherwise use defaults + try: + if overrides['category'] != "": + category = overrides['category'] + else: + category = "Patch - MOS" + except: + category = "Patch - MOS" + + try: + if overrides['software_piece'] != "": + software_piece = overrides['software_piece'] + else: + software_piece = "GI Release Update {0}".format(version) + except: + software_piece = "GI Release Update {0}".format(version) + + try: + if overrides['file_name'] != "": + files = overrides['file_name'] + else: + files = patchfile + except: + files = patchfile + + # Format the new HTML table row + table_row = """\n\n\n\n\n\n""". format( + category=category, + software_piece=software_piece, + files=files + ) + table_found = False + section_found = False + base_found = False # This variable is declared but not used + # Iterate through the documentation lines to find the insertion point + for idx, line in enumerate(documentation): + + if section_found and table_found: + try: + # Check if the line is a table row with a version + if len(line.split(">")) == 3: + version_text = line.split(">")[1].split("<")[0] + # Check if the text is a valid version number + if re.match(r'^[0-9.]+$', version_text): + # Insert the new row before an existing row with a lower version number + if version_text != version and version_text < version: + documentation.insert(idx - 1 , table_row) + print("Inserted row: {0} at line {1} in documentation.\n".format(table_row, idx)) + break + except IndexError: + continue + + # Find the target section header + if line.strip() == "#### Required Oracle Software - Download Summary": + section_found = True + + # Find the table body within the target section + if section_found and line.strip() == "": + table_found = True + + # Stop searching once the end of the table body is reached + if table_found and line.strip() == "": + break + + return documentation + +def rdbms_software_insert_docs(rdbms_software, overrides, documentation): + """Inserts RDBMS software information into the documentation's Markdown table.""" + # Iterate over each RDBMS software entry + for each_patch in rdbms_software: + # Process only the "FREE" edition + if each_patch['edition'] != "FREE": + print("Skipping patch for edition: {0} as it is not FREE edition.\n\n".format(each_patch['edition'])) + break + name = each_patch['name'].split("_")[0] + version = each_patch['version'] + # Separate preinstall and software files + for file in each_patch['files']: + if "preinstall" in file['name'].split("-"): + preinstall_file = file['name'] + continue + else: + software_file = file['name'] + + # Format the new Markdown table row + table_row = "| {0} | {1} | `{2}` | `{3}` |".format( + name.strip(), + version.strip(), + software_file.strip(), + preinstall_file.strip() + ) + + table_found = False + # Iterate through the documentation lines to find the insertion point + for idx, line in enumerate(documentation): + if table_found: + # Insert the new row at the first blank line after the table header + if line.strip() == "": + documentation.insert(idx, table_row + "\n") + print("Inserted row: {0} at line {1} in documentation.\n".format(table_row, idx)) + break + + # Find the Markdown table header + if re.match(r'^\s*\|\s*Product', line.strip()): + table_found = True + + return documentation + +def rdbms_patches_insert_docs(rdbms_patches, overrides, documentation): + """Inserts RDBMS patch information into the documentation's HTML table.""" + # Iterate over each RDBMS patch entry + for each_patch in rdbms_patches: + version = each_patch['release'] + patchfile = each_patch['patchfile'] + + # Use override values from YAML if they exist, otherwise use defaults + try: + if overrides['category'] != "": + category = overrides['category'] + else: + category = "Patch - MOS" + except: + category = "Patch - MOS" + + try: + if overrides['software_piece'] != "": + software_piece = overrides['software_piece'] + else: + software_piece = "Database Release Update {0}".format(version) + except: + software_piece = "Database Release Update {0}".format(version) + + try: + if overrides['file_name'] != "": + files = overrides['file_name'] + else: + files = patchfile + except: + files = patchfile + + # Format the new HTML table row + table_row = """\n\n\n\n\n\n""". format( + category=category, + software_piece=software_piece, + files=files + ) + table_found = False + section_found = False + base_found = False # This variable is declared but not used + # Iterate through the documentation lines to find the insertion point + for idx, line in enumerate(documentation): + + if section_found and table_found: + try: + # Check if the line is a table row with a version + if len(line.split(">")) == 3: + version_text = line.split(">")[1].split("<")[0] + # Check if the text is a valid version number + if re.match(r'^[0-9.]+$', version_text): + # Insert the new row before an existing row with a lower version number + if version_text != version and version_text < version: + documentation.insert(idx - 1 , table_row) + print("Inserted row: {0} at line {1} in documentation.\n".format(table_row, idx)) + break + except IndexError: + continue + + # Find the target section header + if line.strip() == "#### Required Oracle Software - Download Summary": + section_found = True + + # Find the table body within the target section + if section_found and line.strip() == "": + table_found = True + + # Stop searching once the end of the table body is reached + if table_found and line.strip() == "": + break + + return documentation + +def opatch_insert_patch(opatch_patches, overrides, documentation): + """Inserts OPatch information into the documentation's HTML table.""" + # Iterate over each OPatch entry + for each_patch in opatch_patches: + version = each_patch['release'] + patchfile = each_patch['patchfile'] + + # Use override values from YAML if they exist, otherwise use defaults + try: + if overrides['category'] != "": + category = overrides['category'] + else: + category = "" + except: + category = "" + + try: + if overrides['software_piece'] != "": + software_piece = overrides['software_piece'] + else: + software_piece = "OPatch Utility" + except: + software_piece = "OPatch Utility" + + try: + if overrides['file_name'] != "": + files = overrides['file_name'] + else: + files = patchfile + except: + files = patchfile + + # Format the new HTML table row + table_row = """\n\n\n\n\n\n\n""". format( + category=category, + software_piece=software_piece, + files=files + ) + table_found = False + section_found = False + base_found = False # This variable is declared but not used + # Iterate through the documentation lines to find the insertion point + for idx, line in enumerate(documentation): + + if section_found and table_found: + try: + # Check if the line is a table row with a version + if len(line.split(">")) == 3: + version_text = line.split(">")[1].split("<")[0] + # Check if the text is a valid version number + if re.match(r'^[0-9.]+$', version_text): + # Insert the new row before an existing row with a lower version number + if version_text != version and version_text < version: + documentation.insert(idx - 1 , table_row) + print("Inserted row: {0} at line {1} in documentation.\n".format(table_row, idx)) + break + except IndexError: + continue + + # Find the target section header + if line.strip() == "#### Required Oracle Software - Download Summary": + section_found = True + + # Find the table body within the target section + if section_found and line.strip() == "": + table_found = True + + # Stop searching once the end of the table body is reached + if table_found and line.strip() == "": + break + + return documentation + +def main(): + """Main function to drive the documentation update process.""" + # Define paths relative to the script's location + dir_path = pathlib.Path(__file__).parent.parent.parent + input_yml = os.path.join(dir_path, 'modify_patchlist.yaml') + doc_path = os.path.join(dir_path, 'docs/user-guide.md') + + # Load the documentation and the YAML patch data + documentation = load_md(doc_path) + patch_data = load_yaml(input_yml) + + # Check if the documentation update should be skipped + try: + if bool(patch_data['documentation_overrides']['skip_docs_update']): + print("Skipping documentation update as per configuration.\n\n") + sys.exit(0) + except: + print("No skip_docs_update key found in documentation_overrides. Proceeding with documentation update.\n\n") + + # Process GI software if present in the YAML + try: + if patch_data.get('gi_software') is not None: + documentation = gi_software_insert_docs(patch_data['gi_software'], patch_data["documentation_overrides"]['gi_software'], documentation) + except: + print("No 'gi_software' key found in the YAML file. Skipping GI software patch insertion.\n\n") + + # Process RDBMS software if present in the YAML + try: + if patch_data.get('rdbms_software') is not None: + documentation = rdbms_software_insert_docs(patch_data['rdbms_software'], patch_data["documentation_overrides"]['rdbms_software'], documentation) + except: + print("No 'rdbms_software' key found in the YAML file. Skipping RDBMS software patch insertion.\n\n") + + # Process OPatch patches if present in the YAML + try: + if patch_data.get('opatch_patches') is not None: + documentation = opatch_insert_patch(patch_data['opatch_patches'], patch_data["documentation_overrides"]['opatch_patches'], documentation) + except: + print("No 'opatch_patches' key found in the YAML file. Skipping OPatch patches insertion.\n\n") + + # Process GI interim patches if present in the YAML + try: + if patch_data.get('gi_interim_patches') is not None: + documentation = gi_interim_insert_patch(patch_data['gi_interim_patches'], patch_data["documentation_overrides"]['gi_interim_patches'], documentation) + except: + print("No 'gi_interim_patches' key found in the YAML file. Skipping GI interim patches insertion.\n\n") + + # Process RDBMS patches if present in the YAML + try: + if patch_data.get('rdbms_patches') is not None: + documentation = rdbms_patches_insert_docs(patch_data['rdbms_patches'], patch_data["documentation_overrides"]['rdbms_patches'], documentation) + except: + print("No 'rdbms_patches' key found in the YAML file. Skipping RDBMS patches insertion.\n\n") + + # Process GI patches if present in the YAML + try: + if patch_data.get('gi_patches') is not None: + documentation = gi_patches_insert_docs(patch_data['gi_patches'], patch_data["documentation_overrides"]['gi_patches'], documentation) + except: + print("No 'gi_patches' key found in the YAML file. Skipping GI patches insertion.\n\n") + + # Save the modified documentation + save_md(doc_path, ''.join(documentation)) + +# Standard entry point for the script +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/modify_patches.py b/.github/workflows/modify_patches.py new file mode 100644 index 000000000..0c9aa2aa6 --- /dev/null +++ b/.github/workflows/modify_patches.py @@ -0,0 +1,975 @@ +import yaml +import sys, os +import re +import pathlib + + +def load_yaml(file_path): + """ + Loads a YAML file from the given file path. + + Args: + file_path (str): The path to the YAML file. + + Returns: + dict: The parsed YAML data. + """ + try: + # Open and read the YAML file + with open(file_path, 'r') as file: + patch_data = yaml.safe_load(file) + print("YAML Version Data loaded successfully.\n\n") + return patch_data + except FileNotFoundError: + # Handle case where the file does not exist + print(f"Error: '{file_path}' not found.\n\n") + sys.exit(1) + except yaml.YAMLError as exc: + # Handle errors during YAML parsing + print(f"Error parsing YAML file: {exc}\n\n") + sys.exit(1) + +def software_delete_duplicates(match_lines, output_yml): + """ + Deletes entire software patch blocks from the output YAML file based on line numbers. + A software block is identified by a line starting with ' - name:'. + + Args: + match_lines (list): A list of line numbers where duplicates were found. + output_yml (str): The path to the output YAML file. + """ + if not match_lines: + return + with open(output_yml, 'r') as file: + lines = file.readlines() + + # For each match_line, find the start and end of the patch block, then remove it. + # The process is done in reverse order to avoid index shifting issues. + removed_ranges = [] + for match_line in sorted(set(match_lines), reverse=True): + # Find the beginning of the software patch block (a line starting with '- name:') + start = None + for find_range in range(0, 10): + idx = match_line - find_range + if idx < 0: + break + if re.match(r'^ - name:', lines[idx]): + start = idx + break + + # Find the end of the software patch block (the line before the next block or end of file) + end = None + for find_range in range(1, 20): + idx = match_line + find_range + if idx >= len(lines): + break + if re.match(r'^ - name:', lines[idx]): + end = idx + break + if end is None: + end = len(lines) + + if start is not None: + print(f"Removing software patch from line {start} to {end}.\n\n") + removed_ranges.append((start, end)) + else: + print("Error: Could not find the start of the software patch.\n\n") + + # Remove all identified ranges in reverse order to maintain correct indices + for start, end in sorted(removed_ranges, reverse=True): + del lines[start:end] + + # Write the modified content back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + +def patch_delete_duplicates(match_lines, output_yml): + """ + Deletes specific lines from the output YAML file. + + Args: + match_lines (list): A list of line numbers to delete. + output_yml (str): The path to the output YAML file. + """ + if not match_lines: + return + with open(output_yml, 'r') as file: + lines = file.readlines() + + # Delete each matched line, iterating in reverse to avoid index shifting. + for match_line in sorted(set(match_lines), reverse=True): + del lines[match_line] + + # Write the modified content back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + +def gi_software_search_duplicates(gi_software, output_yml): + """ + Searches for and removes duplicate GI software entries in the output YAML file. + + Args: + gi_software (list): A list of GI software patch data. + output_yml (str): The path to the output YAML file. + """ + duplicate_indices = [] + with open(output_yml, 'r') as file: + lines = file.readlines() + for each_patch in gi_software: + name = each_patch['name'].strip() + version = each_patch['version'].strip() + + for idx, line in enumerate(lines): + if idx==0: + skip = True + skip_next_line = False + continue + + if skip_next_line: + skip_next_line = False + continue + + # Match lines for gi_software name and version + name_match = re.match(r'^\s*-\s*name\s*:\s*(.+)$', line) + version_match = re.match(r'^\s*version\s*:\s*(.+)$', line) + + # Skip lines until the 'gi_software:' section is found + if skip: + if line.strip() == 'gi_software:': + skip = False + continue + + # Stop searching within this section if an empty line is encountered + if line.strip() == "": + break + + # If a name or version matches, mark it as a duplicate + if name_match and name_match.group(1).strip() == name: + print(f"GI software '{name}' already exists at line {idx+1}.\n\n") + duplicate_indices.append(idx) + skip_next_line = True + elif version_match and version_match.group(1).strip() == version: + print(f"GI software version '{version}' already exists at line {idx+1}.\n\n") + duplicate_indices.append(idx) + # Remove the identified duplicate software blocks + software_delete_duplicates(duplicate_indices, output_yml) + +def gi_software_compile_patch(gi_software): + """ + Compiles GI software data into a list of formatted YAML strings. + + Args: + gi_software (list): A list of GI software patch data. + + Returns: + list: A list of formatted YAML strings for GI software patches. + """ + patches_list = [] + for each_patch in gi_software: + name = each_patch['name'].strip() + version = each_patch['version'].strip() + files = each_patch['files'] + + # Create the main part of the patch string + patch = " - name: {name}\n version: {version}\n files:\n".format( + name=name, + version=version + ) + + # Add each file to the patch string + for file in files: + patch += """ - {{ name: \"{name}\", sha256sum: \"{sha256}\", md5sum: \"{md5}\", + alt_name: \"{alt_name}\", alt_sha256sum: \"{alt_sha256}\", alt_md5sum: \"{alt_md5}\" }}""".format( + name=file['name'].strip(), + sha256=file['sha256sum'].strip(), + md5=file['md5sum'].strip(), + alt_name=file['alt_name'].strip(), + alt_sha256=file['alt_sha256sum'].strip(), + alt_md5=file['alt_md5sum'].strip() + ) + patches_list.append("\n".join(patch.splitlines())) + patches_list.reverse() # Reverse the list to maintain the original order when inserting + return patches_list + +def gi_software_insert_patch(gi_software_patches, output_yml): + """ + Inserts compiled GI software patches into the output YAML file. + + Args: + gi_software_patches (list): A list of formatted YAML strings for GI software patches. + output_yml (str): The path to the output YAML file. + """ + read_yml = open(output_yml, 'r') + lines = read_yml.readlines() + + # Find the 'gi_software:' line + for i, line in enumerate(lines): + if line.strip() == 'gi_software:': + # Insert each patch after the 'gi_software:' line + for gi_software_patch in gi_software_patches: + lines.insert(i + 1, gi_software_patch + "\n") + break + else: + # Handle case where 'gi_software:' section is not found + print("Error: 'gi_software:' not found in the file.\n\n") + sys.exit(1) + + # Write the updated lines back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + print("GI software patch inserted successfully.\n\n") + +def gi_interim_search_duplicates(gi_interim_patches, output_yml): + """ + Searches for and removes duplicate GI interim patches in the output YAML file. + + Args: + gi_interim_patches (list): A list of GI interim patch data. + output_yml (str): The path to the output YAML file. + """ + duplicate_indices = [] + with open(output_yml, 'r') as file: + lines = file.readlines() + for each_patch in gi_interim_patches: + version = each_patch['version'].strip() + patchnum = each_patch['patchnum'].strip() + + for idx, line in enumerate(lines): + if idx==0: + skip = True + skip_next_line = False + continue + + if skip_next_line: + skip_next_line = False + continue + + # Match lines for gi_interim_patches version and patchnum + version_match = re.match(r'^\s*version\s*:\s*(.+)$', line) + patchnum_match = re.match(r'^\s*patchnum\s*:\s*"?([^"\n]+)"?$', line) + + # Skip lines until the 'gi_interim_patches:' section is found + if skip: + if line.strip() == 'gi_interim_patches:': + skip = False + continue + + # Stop searching within this section if an empty line is encountered + if line.strip() == "": + break + + # If a version or patchnum matches, mark it as a duplicate + if version_match and version_match.group(1).strip() == version: + print(f"GI interim patch version '{version}' already exists at line {idx+1}.\n\n") + duplicate_indices.append(idx) + skip_next_line = True + continue + + if patchnum_match and patchnum_match.group(1).strip() == patchnum: + print(f"GI interim patch '{patchnum}' already exists at line {idx+1}.\n\n") + duplicate_indices.append(idx) + + # Remove the identified duplicate interim patch blocks + gi_interim_delete_duplicates(duplicate_indices, output_yml) + +def gi_interim_delete_duplicates(match_lines, output_yml): + """ + Deletes entire GI interim patch blocks from the output YAML file based on line numbers. + A block is identified by a line starting with ' - category:'. + + Args: + match_lines (list): A list of line numbers where duplicates were found. + output_yml (str): The path to the output YAML file. + """ + if not match_lines: + return + with open(output_yml, 'r') as file: + lines = file.readlines() + + # For each match_line, find the start and end of the patch block, then remove it. + removed_ranges = [] + for match_line in sorted(set(match_lines), reverse=True): + # Find the beginning of the patch block (a line starting with '- category:') + start = None + for find_range in range(0, 10): + idx = match_line - find_range + if idx < 0: + break + if re.match(r'^ - category:', lines[idx]): + start = idx + break + + # Find the end of the patch block (the line before the next block or an empty line) + end = None + for find_range in range(1, 20): + idx = match_line + find_range + if idx >= len(lines): + break + if re.match(r'^ - category:', lines[idx]): + end = idx + break + elif lines[idx].strip() == "": + end = idx + break + + if start is not None: + print(f"Removing GI interim patch from line {start} to {end}.\n\n") + removed_ranges.append((start, end)) + else: + print("Error: Could not find the start of the GI interim patch.\n\n") + + # Remove all identified ranges in reverse order + for start, end in sorted(removed_ranges, reverse=True): + del lines[start:end] + + # Write the modified content back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + +def gi_interim_compile_patch(gi_interim_patches): + """ + Compiles GI interim patch data into a list of formatted YAML strings. + + Args: + gi_interim_patches (list): A list of GI interim patch data. + + Returns: + list: A list of formatted YAML strings for GI interim patches. + """ + patches_list = [] + for each_patch in gi_interim_patches: + category = each_patch['category'].strip() + version = each_patch['version'].strip() + patchnum = each_patch['patchnum'].strip() + patchutil = each_patch['patchutil'].strip() + files = each_patch['files'] + + # Create the main part of the patch string + patch = " - category: \"{0}\"\n version: {1}\n patchnum: \"{2}\"\n patchutil: \"{3}\"\n files:\n".format( + category, version, patchnum, patchutil + ) + + # Add each file to the patch string + for file in files: + patch += " - {{ name: \"{0}\", sha256sum: \"{1}\", md5sum: \"{2}\" }}\n".format( + file['name'].strip(), + file['sha256sum'].strip(), + file['md5sum'].strip() + ) + patches_list.append("\n".join(patch.splitlines())) + + return patches_list + +def gi_interim_insert_patch(gi_interim_patches, output_yml): + """ + Inserts compiled GI interim patches into the output YAML file. + + Args: + gi_interim_patches (list): A list of formatted YAML strings for GI interim patches. + output_yml (str): The path to the output YAML file. + """ + read_yml = open(output_yml, 'r') + lines = read_yml.readlines() + + # Find the 'gi_interim_patches:' line + for i, line in enumerate(lines): + if line.strip() == 'gi_interim_patches:': + # Insert each patch after the 'gi_interim_patches:' line + for gi_interim_patch in gi_interim_patches: + lines.insert(i+1, gi_interim_patch + "\n") + break + + # Write the updated lines back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + print("GI interim patches patch inserted successfully.\n\n") + +def rdbms_software_search_duplicates(rdbms_software, output_yml): + """ + Searches for and removes duplicate RDBMS software entries in the output YAML file. + + Args: + rdbms_software (list): A list of RDBMS software patch data. + output_yml (str): The path to the output YAML file. + """ + duplicate_indices = [] + with open(output_yml, 'r') as file: + lines = file.readlines() + for each_patch in rdbms_software: + name = each_patch['name'].strip() + version = each_patch['version'].strip() + + for idx, line in enumerate(lines): + if idx==0: + skip = True + skip_next_line = False + continue + + if skip_next_line: + skip_next_line = False + continue + + # Match lines for rdbms_software name and version + name_match = re.match(r'^\s*-\s*name\s*:\s*(.+)$', line) + version_match = re.match(r'^\s*version\s*:\s*(.+)$', line) + + # Skip lines until the 'rdbms_software:' section is found + if skip: + if line.strip() == 'rdbms_software:': + skip = False + continue + + # Stop searching within this section if an empty line is encountered + if line.strip() == "": + break + + # If a name or version matches, mark it as a duplicate + if name_match and name_match.group(1).strip() == name: + print(f"GI software '{name}' already exists at line {idx+1}.\n\n") + duplicate_indices.append(idx) + skip_next_line = True + elif version_match and version_match.group(1).strip() == version: + print(f"GI software version '{version}' already exists at line {idx+1}.\n\n") + duplicate_indices.append(idx) + + # Remove the identified duplicate software blocks + software_delete_duplicates(duplicate_indices, output_yml) + +def rdbms_software_compile_patch(rdbms_software): + """ + Compiles RDBMS software data into a list of formatted YAML strings. + + Args: + rdbms_software (list): A list of RDBMS software patch data. + + Returns: + list: A list of formatted YAML strings for RDBMS software patches. + """ + patches_list = [] + for each_patch in rdbms_software: + name = each_patch['name'].strip() + version = each_patch['version'].strip() + edition = each_patch['edition'] + files = each_patch['files'] + + # Handle both list and single string for 'edition' + if isinstance(edition, list): + patch = " - name: {0}\n version: {1}\n edition:\n".format(name, version) + patch += "\n".join([" - {0}".format(e.strip()) for e in edition]) + patch += "\n files:" + else: + patch = " - name: {0}\n version: {1}\n edition: {2}\n files:".format(name, version, edition.strip()) + + # Add each file to the patch string + for file in files: + patch += "\n - {{ name: \"{0}\", sha256sum: \"{1}\", md5sum: \"{2}\" }}".format( + file['name'].strip(), + file['sha256sum'].strip(), + file['md5sum'].strip() + ) + patches_list.append("\n".join(patch.splitlines())) + return patches_list + +def rdbms_software_insert_patch(rdbms_software_patches, output_yml): + """ + Inserts compiled RDBMS software patches into the output YAML file. + + Args: + rdbms_software_patches (list): A list of formatted YAML strings for RDBMS software patches. + output_yml (str): The path to the output YAML file. + """ + read_yml = open(output_yml, 'r') + lines = read_yml.readlines() + + # Find the 'rdbms_software:' line + for i, line in enumerate(lines): + if line.strip() == 'rdbms_software:': + # Insert each patch after the 'rdbms_software:' line + for rdbms_software_patch in rdbms_software_patches: + lines.insert(i + 1, rdbms_software_patch + "\n") + break + else: + # Handle case where 'rdbms_software:' section is not found + print("Error: 'rdbms_software:' not found in the file.\n\n") + sys.exit(1) + + # Write the updated lines back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + print("RDBMS software patch inserted successfully.\n\n") + +def opatch_patch_search_duplicates(opatch_patches, output_yml): + """ + Searches for and removes duplicate OPatch entries in the output YAML file. + + Args: + opatch_patches (list): A list of OPatch patch data. + output_yml (str): The path to the output YAML file. + """ + for patch in opatch_patches: + release = patch['release'].strip() + patchnum = patch['patchnum'].strip() + duplicate_indices = [] + skip = True + + with open(output_yml, 'r') as file: + lines = file.readlines() + for idx, line in enumerate(lines): + # Use regex to find the release value in a line + release_match = re.search(r'release\s*:\s*"?([^",}]+)"?', line) + + if idx==0: + skip = True + continue + + # Skip lines until the 'opatch_patches:' section is found + if skip: + if line.strip() == 'opatch_patches:': + skip = False + continue + + # Stop searching within this section if an empty line is encountered + if line.strip()=="": + break + + # If a release matches, mark the line as a duplicate + if release_match and release_match.group(1).strip() == release: + print(f"OPatch patch with release '{release}' already exists at line {idx}.\n\n") + duplicate_indices.append(idx) + continue + + # Remove the identified duplicate lines + patch_delete_duplicates(duplicate_indices, output_yml) + +def opatch_patch_compile_patch(opatch_patches): + """ + Compiles OPatch data into a list of formatted YAML strings. + + Args: + opatch_patches (list): A list of OPatch patch data. + + Returns: + list: A list of formatted YAML strings for OPatch patches. + """ + patches_list = [] + for patches in opatch_patches: + # Format each patch as a single-line YAML list item + patches_list.append(" - {{ category: \"OPatch\", release: \"{0}\", patchnum: \"{1}\", patchfile: \"{2}\", md5sum: \"{3}\" }}\n".format( + patches['release'].strip(), + patches['patchnum'].strip(), + patches['patchfile'].strip(), + patches['md5sum'].strip() + ) + ) + return patches_list + +def opatch_patch_insert_patch(opatch_patches_patch, output_yml): + """ + Inserts compiled OPatch patches into the output YAML file. + + Args: + opatch_patches_patch (list): A list of formatted YAML strings for OPatch patches. + output_yml (str): The path to the output YAML file. + """ + with open(output_yml, 'r') as file: + lines = file.readlines() + + opatch_start = None + opatch_end = None + + # Find the start of the opatch_patches list + for i, line in enumerate(lines): + if line.strip() == 'opatch_patches:': + opatch_start = i + break + + if opatch_start is None: + print("Error: 'opatch_patches:' not found in the file.\n\n") + sys.exit(1) + + # Find the end of the opatch_patches list (the line before the next top-level key) + for j in range(opatch_start + 1, len(lines)): + if re.match(r'^\S', lines[j]) and not lines[j].strip().startswith('-'): + opatch_end = j-1 + break + if opatch_end is None: + opatch_end = len(lines) + + # Insert at the end of the opatch_patches list + insert_pos = opatch_end + for patch in opatch_patches_patch: + lines.insert(insert_pos, patch) + insert_pos += 1 + + # Write the updated lines back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + print("OPatch patches patch appended successfully.\n\n") + +def gi_patch_search_duplicates(gi_patches, output_yml): + """ + Searches for and removes duplicate GI patches in the output YAML file. + + Args: + gi_patches (list): A list of GI patch data. + output_yml (str): The path to the output YAML file. + """ + for patch in gi_patches: + release = patch['release'].strip() + patchnum = patch['patchnum'].strip() + duplicate_indices = [] + skip = True + + with open(output_yml, 'r') as file: + lines = file.readlines() + for idx, line in enumerate(lines): + # Ignore commented lines + if line.strip().startswith('#'): + continue + + # Skip lines until the 'gi_patches:' section is found + if skip: + if line.strip() == 'gi_patches:': + skip = False + continue + + # Stop searching within this section if an empty line is encountered + if line.strip()=="": + skip = True + break + + # Parse the line as YAML to check its contents + line_yaml = yaml.safe_load(line.strip()) + + # If release or patchnum matches, mark the line as a duplicate + if line_yaml[0]['release'].strip() == patch['release'].strip(): + print(f"GI patch with release '{release}' already exists at line {idx}.\n\n") + duplicate_indices.append(idx) + if line_yaml[0]['patchnum'].strip() == patch['patchnum'].strip(): + print(f"GI patch with patchnum '{patchnum}' already exists at line {idx}.\n\n") + duplicate_indices.append(idx) + + # Remove the identified duplicate lines + patch_delete_duplicates(set(duplicate_indices), output_yml) + +def gi_patches_insert_patch(gi_patches, output_yml): + """ + Inserts GI patches into the output YAML file, grouped by category and base. + + Args: + gi_patches (list): A list of GI patch data. + output_yml (str): The path to the output YAML file. + """ + with open(output_yml, 'r') as file: + lines = file.readlines() + gi_patch_start = None + + # Find the start and end of the gi_patches block + for idx, line in enumerate(lines): + if line.strip() == 'gi_patches:': + gi_patch_start = idx + 1 + + if gi_patch_start is not None and line.strip() == "": + gi_patch_end = idx + 2 + break + + category_match_found = False + category_base_match_found = False + idx = gi_patch_start + + for patch in gi_patches: + while idx < gi_patch_end: + if category_base_match_found is False and lines[idx].strip() == "": + print("Error: Empty line found in gi_patches block.\n\n") + return + + if lines[idx].startswith('# -'): + idx += 1 + continue + + if idx == gi_patch_end: + print("Error: Reached end of gi_patches block without finding a match.\n\n") + return + + # Parse the line as YAML to inspect its properties + line = yaml.safe_load(lines[idx]) + + # Check for matching category + if line != None and line[0]['category'] == patch['category'].strip(): + category_match_found = True + + # If category matches, check for matching base + if line != None and category_match_found and line[0]['base'] == patch['base'].strip(): + category_base_match_found = True + + # If both category and base match, find the insertion point (end of the sub-group) + if category_base_match_found and lines[idx].strip() == "" or category_base_match_found and lines[idx].startswith('#') or category_base_match_found and lines[idx].strip() == "": + # Insert the new patch at the current index + lines.insert(idx, " - {{ category: \"{category}\", base: \"{base}\", release: \"{release}\", patchnum: \"{patchnum}\", patchfile: \"{patchfile}\", patch_subdir: \"{patch_subdir}\", prereq_check: {prereq_check}, method: \"{method}\", ocm: {ocm}, upgrade: {upgrade}, md5sum: \"{md5sum}\" }}\n".format( + category=patch['category'].strip(), + base=patch['base'].strip(), + release=patch['release'].strip(), + patchnum=patch['patchnum'].strip(), + patchfile=patch['patchfile'].strip(), + patch_subdir=patch['patch_subdir'].strip(), + prereq_check=str(patch['prereq_check']).lower(), + method=patch['method'].strip(), + ocm=str(patch['ocm']).lower(), + upgrade=str(patch['upgrade']).lower(), + md5sum=patch['md5sum'].strip(), + )) + print("Inserted GI patch at line {0}.\n\n".format(idx + 1)) + # Reset flags and index for the next patch + category_match_found = False + category_base_match_found = False + idx = gi_patch_start + break + + idx += 1 + if idx == gi_patch_end: + print("Error: Reached end of gi_patches block without finding a match.\n\n") + return + + # Write the updated lines back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + print("GI patches patch inserted successfully.\n\n") + +def rdbms_patch_search_duplicates(rdbms_patches, output_yml): + """ + Searches for and removes duplicate RDBMS patches in the output YAML file. + + Args: + rdbms_patches (list): A list of RDBMS patch data. + output_yml (str): The path to the output YAML file. + """ + for patch in rdbms_patches: + release = patch['release'].strip() + patchnum = patch['patchnum'].strip() + duplicate_indices = [] + skip = True + + with open(output_yml, 'r') as file: + lines = file.readlines() + for idx, line in enumerate(lines): + # Ignore commented lines + if line.strip().startswith('#'): + continue + + # Skip lines until the 'rdbms_patches:' section is found + if skip: + if line.strip() == 'rdbms_patches:': + skip = False + continue + + # Stop searching within this section if an empty line is encountered + if line.strip()=="": + skip = True + break + + # Parse the line as YAML to check its contents + line_yaml = yaml.safe_load(line.strip()) + + # If category and release/patchnum match, mark the line as a duplicate + if line_yaml[0]['category'].strip() == patch['category'] and line_yaml[0]['release'].strip() == patch['release'].strip() or line_yaml[0]['category'].strip() == patch['category'] and line_yaml[0]['patchnum'].strip() == patch['patchnum'].strip(): + print(f"RDBMS patch with release '{release}' already exists at line {idx}.\n\n") + duplicate_indices.append(idx) + if line_yaml[0]['patchnum'].strip() == patch['patchnum'].strip(): + print(f"RDBMS patch with patchnum '{patchnum}' already exists at line {idx}.\n\n") + duplicate_indices.append(idx) + + # Remove the identified duplicate lines + patch_delete_duplicates(set(duplicate_indices), output_yml) + +def rdbms_patches_insert_patch(rdbms_patches, output_yml): + """ + Inserts RDBMS patches into the output YAML file, grouped by category and base. + + Args: + rdbms_patches (list): A list of RDBMS patch data. + output_yml (str): The path to the output YAML file. + """ + with open(output_yml, 'r') as file: + lines = file.readlines() + rdbms_patch_start = None + rdbms_patch_end = None + + # Find the start and end of the rdbms_patches block + for idx, line in enumerate(lines): + if line.strip() == 'rdbms_patches:': + rdbms_patch_start = idx + 1 + + if rdbms_patch_start is not None and line.strip() == "": + rdbms_patch_end = idx + 2 + break + + if idx == len(lines) - 1 and rdbms_patch_start is not None: + # If we reach the end of the file, the block ends here + rdbms_patch_end = idx + 1 + + if rdbms_patch_start is None or rdbms_patch_end is None: + print("Error: 'rdbms_patches:' not found in the file or no empty line after it.\n\n") + sys.exit(1) + + category_match_found = False + category_base_match_found = False + idx = rdbms_patch_start + + for patch in rdbms_patches: + while idx <= rdbms_patch_end: + # If we found the right group and reached its end, insert the new patch + if category_base_match_found and idx == rdbms_patch_end or category_base_match_found and lines[idx].strip() == "" or idx==rdbms_patch_end: + # Insert the new patch at the current index + lines.insert(idx, " - {{ category: \"{category}\", base: \"{base}\", release: \"{release}\", patchnum: \"{patchnum}\", patchfile: \"{patchfile}\", patch_subdir: \"{patch_subdir}\", prereq_check: {prereq_check}, method: \"{method}\", ocm: {ocm}, upgrade: {upgrade}, md5sum: \"{md5sum}\" }}\n".format( + category=patch['category'].strip(), + base=patch['base'].strip(), + release=patch['release'].strip(), + patchnum=patch['patchnum'].strip(), + patchfile=patch['patchfile'].strip(), + patch_subdir=patch['patch_subdir'].strip(), + prereq_check=str(patch['prereq_check']).lower(), + method=patch['method'].strip(), + ocm=str(patch['ocm']).lower(), + upgrade=str(patch['upgrade']).lower(), + md5sum=patch['md5sum'].strip(), + )) + print("Inserted RDBMS patch at line {0}.\n\n".format(idx + 1)) + # Reset flags and index for the next patch + category_match_found = False + category_base_match_found = False + idx = rdbms_patch_start + rdbms_patch_end += 1 # Adjust end index because we added a line + break + + if category_base_match_found is False and lines[idx].strip() == "": + print("Error: Empty line found in rdbms_patches block.\n\n") + return + + if lines[idx].startswith('# -'): + idx += 1 + continue + + if idx == rdbms_patch_end: + print("Error: Reached end of rdbms_patches block without finding a match.\n\n") + return + + # Parse the line as YAML to inspect its properties + line = yaml.safe_load(lines[idx]) + + # Check for matching category + if line != None and line[0]['category'] == patch['category'].strip(): + category_match_found = True + + # If category matches, check for matching base + if line != None and category_match_found and line[0]['base'] == patch['base'].strip(): + category_base_match_found = True + + idx += 1 + + # Write the updated lines back to the file + with open(output_yml, 'w') as file: + file.writelines(lines) + print("RDBMS patches patch inserted successfully.\n\n") + +def comment_after_completed_patch(input_yml): + """ + Comments out all lines in the input YAML file to prevent re-processing. + + Args: + input_yml (str): The path to the input YAML file. + """ + with open(input_yml, 'r') as file: + lines = file.readlines() + + # Iterate through lines and comment them out, skipping section headers and empty lines + for i, line in enumerate(lines): + if line.strip() == 'gi_software:' or line.strip() == 'gi_interim_patches:' or line.strip() == 'rdbms_software:' or line.strip() == 'opatch_patches:' or line.strip() == 'gi_patches:' or line.strip() == 'rdbms_patches:' or line.strip() == 'documentation_overrides:' or line.strip().startswith("skip_docs_update") or line.strip() == '': + continue + if line.strip().startswith('#'): + continue + else: + lines[i] = "# " + lines[i] + # Write the commented lines back to the file + with open(input_yml, 'w') as file: + file.writelines(lines) + print("Comment added after completed patches.\n\n") + +def main(): + """ + Main function to orchestrate the patch modification process. + """ + # Define file paths relative to the script location + dir_path = pathlib.Path(__file__).parent.parent.parent + input_yml = os.path.join(dir_path, 'modify_patchlist.yaml') + output_yml = os.path.join(dir_path, 'roles/common/defaults/main.yml') + + # Load the patch data from the input file + patch_data = load_yaml(input_yml) + + # Validate the output YAML file + try: + yaml.safe_load(open(output_yml, 'r')) + except yaml.YAMLError as exc: + print(f"Error parsing YAML file: {exc}\n\n") + sys.exit(1) + + if patch_data is None: + print("No patch data found in the YAML file.\n\n") + sys.exit(1) + + # Process GI software patches if they exist + try: + if patch_data.get('gi_software') is not None: + gi_software_search_duplicates(patch_data['gi_software'], output_yml) + gi_software_insert_patch(gi_software_compile_patch(patch_data['gi_software']), output_yml) + except KeyError: + print("No 'gi_software' key found in the YAML file. Skipping GI software patch insertion.\n\n") + + # Process GI interim patches if they exist + try: + if patch_data.get('gi_interim_patches') is not None: + gi_interim_search_duplicates(patch_data['gi_interim_patches'], output_yml) + gi_interim_insert_patch(gi_interim_compile_patch(patch_data['gi_interim_patches']), output_yml) + except KeyError: + print("No 'gi_interim_patches' key found in the YAML file. Skipping GI interim patches patch insertion.\n\n") + + # Process RDBMS software patches if they exist + try: + if patch_data.get('rdbms_software') is not None: + rdbms_software_search_duplicates(patch_data['rdbms_software'], output_yml) + rdbms_software_insert_patch(rdbms_software_compile_patch(patch_data['rdbms_software']),output_yml) + except KeyError: + print("No 'rdbms_software' key found in the YAML file. Skipping RDBMS software patch insertion.\n\n") + + # Process OPatch patches if they exist + try: + if patch_data.get('opatch_patches') is not None: + opatch_patch_search_duplicates(patch_data['opatch_patches'], output_yml) + opatch_patch_insert_patch(opatch_patch_compile_patch(patch_data['opatch_patches']),output_yml) + except KeyError: + print("No 'opatch_patches' key found in the YAML file. Skipping OPatch patches patch insertion.\n\n") + + # Process GI patches if they exist + try: + if patch_data.get('gi_patches') is not None: + gi_patch_search_duplicates(patch_data['gi_patches'], output_yml) + gi_patches_insert_patch(patch_data['gi_patches'], output_yml) + except KeyError: + print("No 'gi_patches' key found in the YAML file. Skipping GI patches patch insertion.\n\n") + + # Process RDBMS patches if they exist + try: + if patch_data.get('rdbms_patches') is not None: + rdbms_patch_search_duplicates(patch_data['rdbms_patches'], output_yml) + rdbms_patches_insert_patch(patch_data['rdbms_patches'], output_yml) + except KeyError: + print("No 'rdbms_patches' key found in the YAML file. Skipping RDBMS patches patch insertion.\n\n") + + # Comment out the input file to prevent re-running + comment_after_completed_patch(input_yml) + +if __name__ == "__main__": + main() + + + diff --git a/.github/workflows/modify_patches.yml b/.github/workflows/modify_patches.yml new file mode 100644 index 000000000..8ad23b872 --- /dev/null +++ b/.github/workflows/modify_patches.yml @@ -0,0 +1,43 @@ +name: Modify Patches +run-name: Modify Patches + +on: + # Triggers the workflow on push or pull request events but only for the "master" branch + pull_request: + branches: [ "master" ] + workflow_dispatch: + +jobs: + build-and-commit: + runs-on: ubuntu-latest + steps: + - name: Checkout PR Branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13.5' + cache: 'pip' # Cache dependencies for faster runs + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyyaml + + - name: Change documentation files + run: python .github/workflows/modify_documentation.py + + - name: Change patch files + run: python .github/workflows/modify_patches.py + + - name: Commit and push changes + uses: stefanzweifel/git-auto-commit-action@v6 + with: + commit_message: "automation: Update patch files" + commit_user_name: "GitHub Actions" + commit_user_email: "actions@github.com" + commit_author: Author + branch: ${{ github.head_ref }} \ No newline at end of file diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt new file mode 100644 index 000000000..4818cc541 --- /dev/null +++ b/.github/workflows/requirements.txt @@ -0,0 +1 @@ +pyyaml \ No newline at end of file diff --git a/modify_patchlist.md b/modify_patchlist.md new file mode 100644 index 000000000..e7d7458a3 --- /dev/null +++ b/modify_patchlist.md @@ -0,0 +1,139 @@ +# Patch Automation User Guide + +## How to Add a New Patch + +The entire process is driven by a single input file: `modify_patchlist.yaml`. + +### Step 1: Edit the Input File + +Open the `modify_patchlist.yaml` file located in the root of the repository. This file contains several sections for different types of software and patches. + +### Step 2: Find the Correct Section + +Locate the section corresponding to the type of software or patch you are adding. The available sections are: + +* `gi_software`: Oracle Grid Infrastructure base installation media. +* `rdbms_software`: Oracle RDBMS base installation media (e.g., FREE edition RPMs). +* `opatch_patches`: OPatch utility versions. +* `gi_interim_patches`: One-off interim patches for Grid Infrastructure. +* `gi_patches`: Cumulative patches (Release Updates) for Grid Infrastructure. +* `rdbms_patches`: Cumulative patches (Release Updates) for RDBMS. + +### Step 3: Add the New Entry + +Add your new software or patch information as a new list item under the appropriate section. Ensure your entry follows the existing YAML format for that section. + +#### Example: Adding a new RDBMS Release Update + +To add a new RDBMS patch, you would add a new entry under the `rdbms_patches` section in `modify_patchlist.yaml`: + +```yaml +rdbms_patches: + - { category: "RU", base: "21.3.0.0.0", release: "21.19.0.0.0", patchnum: "38123456", patchfile: "p38123456_210000_Linux-x86-64.zip", patch_subdir: "/", prereq_check: false, method: "opatch apply", ocm: false, upgrade: false, md5sum: "newMd5SumGoesHere==" } +``` + +**Important:** + +* The automation will automatically detect and remove any older versions of the same patch (based on `release` or `patchnum`) to prevent duplicates. +* Fill in all fields accurately, especially the checksums. + +### Step 4: (Optional) Override Documentation Text + +The automation will also update the user documentation. It generates default descriptions based on the patch data. If you need to override this default text, you can specify custom values in the `documentation_overrides` section of `modify_patchlist.yaml`. + +**Example:** + +```yaml +documentation_overrides: + skip_docs_update: false # Set to true to skip doc updates entirely + + rdbms_patches: + category: "Patch - My Oracle Support" + software_piece: "Custom DB RU 21.19" + file_name: "p38123456_210000_Linux-x86-64.zip" +``` + +If you leave these fields blank, the automation will use values based on patch applied. + +### Step 5: Commit and Push + +Commit and push your changes to the `modify_patchlist.yaml` file. + +```bash +git add modify_patchlist.yaml +git commit -m "feat: Add RDBMS RU 21.19.0.0.0" +git push +``` + +### What Happens Next? + +Once you create a PR into master, the Git automation will trigger. It will: + +1. Read your new entries from `modify_patchlist.yaml`. +2. Update the main configuration file (`roles/common/defaults/main.yml`) with the new patch data. +3. Update the documentation tables in `docs/user-guide.md`. +4. Comment out the entries you added in `modify_patchlist.yaml` to mark them as processed. +5. Commit all these changes back to your branch automatically. + +You will see a new commit on your branch authored by "GitHub Actions" with the message "automation: Update patch files". + +--- + +## Technical Documentation + +This section details the internal workings of the automation, its components, and the overall workflow. + +### Automation Flow + +The automation is executed within a GitHub Actions workflow and follows these steps: + +1. **Trigger**: The workflow is triggered by a push to the repository that includes changes to `modify_patchlist.yaml`. +2. **Update Configuration**: The `.github/workflows/modify_patches.py` script is executed. + * It parses the new, uncommented entries in `modify_patchlist.yaml`. + * For each entry, it searches `roles/common/defaults/main.yml` for existing duplicates (e.g., by version, name, or patch number). + * It removes any found duplicates to ensure the configuration remains clean. + * It formats and inserts the new patch data into the correct list within `roles/common/defaults/main.yml`. + * After processing all entries, it comments out the processed lines in `modify_patchlist.yaml` to prevent them from being processed again on subsequent runs. +3. **Update Documentation**: The `.github/workflows/modify_documentation.py` script is executed. + * It also parses the new entries from `modify_patchlist.yaml`. + * It checks for any documentation overrides provided in the `documentation_overrides` section. + * It formats the new software/patch information into a new table row (HTML or Markdown, depending on the target table). + * It intelligently inserts the new row into the correct table in `docs/user-guide.md`, attempting to maintain version-based sorting. +4. **Commit Changes**: The `.github/workflows/commit_patches.bash` script is executed as the final step. + * It configures git with a default user ("GitHub Actions"). + * It stages all modified files (`roles/common/defaults/main.yml`, `docs/user-guide.md`, and `modify_patchlist.yaml`). + * It commits the staged changes with a standardized commit message. + * It pushes the new commit back to the repository, completing the cycle. + +### File Breakdown + +The automation involves the following key files: + +| File Path | Description | +| :--- | :--- | +| `modify_patchlist.yaml` | **Input File.** Users edit this file to add new software or patches. It acts as a temporary manifest for the automation to consume. | +| `roles/common/defaults/main.yml`| **Primary Configuration Target.** This Ansible defaults file is the "source of truth" for software/patch definitions and is automatically updated by the automation. | +| `docs/user-guide.md` | **Documentation Target.** The user guide containing software download tables, which is automatically updated to reflect new additions. | +| `.github/workflows/modify_patches.py`| **Core Logic Script.** Responsible for parsing the input file, handling de-duplication, and updating `roles/common/defaults/main.yml`. It also comments out processed entries in the input file. | +| `.github/workflows/modify_documentation.py` | **Documentation Script.** Responsible for parsing the input file and inserting new entries into the appropriate tables in `docs/user-guide.md`. | +| `.github/workflows/commit_patches.bash` | **Git Commit Script.** A simple shell script that finalizes the automation by committing all the generated file changes back to the repository. | + +### Script Details + +#### `modify_patches.py` + +This script orchestrates the update of the main Ansible configuration file. + +* **De-duplication**: Before adding a new patch, the script reads `main.yml` line by line to find entries with a matching `version`, `release`, or `patchnum`. If a match is found, the entire block for the old patch is removed to prevent conflicts and superseded entries. +* **Patch Compilation**: It constructs the new YAML entry as a properly formatted, single-line string to ensure consistent styling. +* **Insertion**: The script locates the correct top-level key (e.g., `gi_patches:`) and inserts the new patch entry at the beginning of the list. This keeps the most recent patches at the top. +* **idempotency**: By commenting out the processed lines in `modify_patchlist.yaml` after a successful run, the script ensures that a re-run of the workflow does not process the same patches again. + +#### `modify_documentation.py` + +This script handles the automated updates to the user-facing documentation. + +* **Data Loading**: It loads the patch data from `modify_patchlist.yaml` and the documentation content from `docs/user-guide.md`. +* **Overrides**: It checks for and applies any user-defined overrides from the `documentation_overrides` section of the input file. +* **Table Insertion**: The script contains logic to find specific tables within the markdown file (e.g., "Required Oracle Software - Download Summary"). It then generates a new table row and inserts it. For tables sorted by version, it attempts to insert the new row in the correct chronological position. +* **File Saving**: After all modifications, it overwrites `docs/user-guide.md` with the updated content. diff --git a/modify_patchlist.yaml b/modify_patchlist.yaml new file mode 100644 index 000000000..14f6b95da --- /dev/null +++ b/modify_patchlist.yaml @@ -0,0 +1,105 @@ +# Oracle Software and Patch Manifest + +# This file serves as a centralized manifest for Oracle software and patch information. +# It defines the necessary files, versions, and checksums required for automated +# deployment and patching of Oracle Grid Infrastructure (GI) and RDBMS. + +gi_software: +# Defines the base installation media for Oracle Grid Infrastructure. +# - name: A unique identifier for the software version. +# - version: The official version number. +# - files: A list of software archive files with their checksums. +## Example entries are commented out for reference. + # - name: 21c_gi + # version: 21.3.0.0.0 + # files: + # - { name: "V1011504-01.zip", sha256sum: "070D4471BC067B1290BDCEE6B1C1FFF2F21329D2839301E334BCB2A3D12353A3", md5sum: "s/vbdiGtgsvU9AlD7/3Rvg==", + # alt_name: "LINUX.X64_213000_grid_home.zip", alt_sha256sum: "070D4471BC067B1290BDCEE6B1C1FFF2F21329D2839301E334BCB2A3D12353A3", alt_md5sum: "s/vbdiGtgsvU9AlD7/3Rvg==" } + +rdbms_software: +# Defines the base installation media for Oracle RDBMS. +# - name: A unique identifier for the software version. +# - version: The official version number. +# - edition: The database edition (e.g., FREE, EE). +# - files: A list of software files (e.g., RPMs) with their checksums. +## Example entries are commented out for reference. + # - name: 23ai_free_23_8 + # version: 23.8.0.25.04 + # edition: FREE + # files: + # - { name: "oracle-database-preinstall-23ai-1.0-2.el8.x86_64.rpm", sha256sum: "4578e6d1cf566e04541e0216b07a0372725726a7c339423ee560255cb918138b", md5sum: "TmjqUT878Owv7NbXGECpTA=="} + # - { name: "oracle-database-free-23ai-23.8-1.el8.x86_64.rpm", sha256sum: "cd0d16939150e6ec5e70999a762a13687bfa99b05c4f310593e7ca3892e1d0ce", md5sum: "hkL/hxeYbB7z5lz+3r3kww=="} + +opatch_patches: +# Lists the required versions of the OPatch utility. Different Oracle +# releases may require specific OPatch versions for applying patches. +# - release: The Oracle Home version this OPatch is intended for. +# - patchnum: The patch number for the OPatch utility itself. +## Example entries are commented out for reference. + # - { category: "OPatch", release: "19.3.0.0.0", patchnum: "6880880", patchfile: "p6880880_190000_Linux-x86-64.zip", md5sum: "" } + # - { category: "OPatch", release: "21.3.0.0.0", patchnum: "6880880", patchfile: "p6880880_210000_Linux-x86-64.zip", md5sum: "" } + +gi_interim_patches: +# Lists any one-off interim patches for Grid Infrastructure that need to be +# applied separately from the main Release Update (RU). +## Example entries are commented out for reference. + # - category: "HAS_interim_patch" + # version: 12.2.0.1.0 + # patchnum: "25078431" + # patchutil: "gridsetup" + # files: + # - { name: "p25078431_122010_Linux-x86-64.zip", sha256sum: "FA056EBD0FE0AD134F2B5C53F0DDF6F6A5DC73C7AE40DAEF18D0629850149525", md5sum: "hK1WOGC1g/3QUryg3MM5OQ==" } + +gi_patches: +# Defines the cumulative patches, such as Release Updates (RU), for +# Grid Infrastructure. +# - base: The base version the patch applies to. +# - release: The target version after the patch is applied. +# - method: The command used to apply the patch (e.g., "opatchauto apply"). +## Example entries are commented out for reference. + # - { category: "RU", base: "21.3.0.0.0", release: "21.17.0.0.0", patchnum: "37349593", patchfile: "p37349593_210000_Linux-x86-64.zip", patch_subdir: "/", prereq_check: false, method: "opatchauto apply", ocm: false, upgrade: false, md5sum: "Mt0Bw+IPKqoKh31YkneCrg==" } + # - { category: "RU", base: "21.3.0.0.0", release: "21.18.0.0.0", patchnum: "37642955", patchfile: "p37642955_210000_Linux-x86-64.zip", patch_subdir: "/", prereq_check: false, method: "opatchauto apply", ocm: false, upgrade: false, md5sum: "b8YrXNXis6agqIHny746Xw==" } + +rdbms_patches: +# Defines the cumulative patches, such as Release Updates (RU), for RDBMS. +# - method: The command used to apply the patch (e.g., "opatch apply"). +## Example entries are commented out for reference. + # - { category: "RU", base: "21.3.0.0.0", release: "21.17.0.0.0", patchnum: "37350281", patchfile: "p37350281_210000_Linux-x86-64.zip", patch_subdir: "/", prereq_check: false, method: "opatch apply", ocm: false, upgrade: false, md5sum: "dQjBqlXWOumEZU3QAJzD9Q==" } + - { category: "RU", base: "19.0.0.0.0", release: "19.18.0.0.0", patchnum: "37960098", patchfile: "p37960098_190000_Linux-x86-64.zip", patch_subdir: "/", prereq_check: false, method: "opatch apply", ocm: false, upgrade: false, md5sum: "4GhJvWSOeDMYUIupR0jFZA==" } +documentation_overrides: +# An optional section to control automated documentation generation. +# - skip_docs_update: Set to true to prevent any documentation updates. +# - Other keys can be populated to override specific documentation entries +# for a given software or patch file. If left blank, default values are used. +# + skip_docs_update: false + + gi_software: + category: "" + software_piece: "" + file_name: "" + + rdbms_software: + category: "" + software_piece: "" + file_name: "" + + opatch_patches: + category: "" + software_piece: "" + file_name: "" + + gi_interim_patches: + category: "" + software_piece: "" + file_name: "" + + gi_patches: + category: "" + software_piece: "" + file_name: "" + + rdbms_patches: + category: "" + software_piece: "" + file_name: "" \ No newline at end of file diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index 9d3d5228b..6a8bfd4cf 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -342,8 +342,6 @@ gi_patches: - { category: "RU", base: "19.3.0.0.0", release: "19.26.0.0.250121", patchnum: "37262208", patchfile: "p37262208_190000_Linux-x86-64.zip", patch_subdir: "/37257886", prereq_check: false, method: "opatchauto apply", ocm: false, upgrade: false, md5sum: "zqZeJ3/ujDWcLr+reZOHpw==" } - { category: "RU", base: "19.3.0.0.0", release: "19.27.0.0.250415", patchnum: "37591516", patchfile: "p37591516_190000_Linux-x86-64.zip", patch_subdir: "/37641958", prereq_check: false, method: "opatchauto apply", ocm: false, upgrade: false, md5sum: "kkyZC9RP8eolBgdu9Y3q7g==" } - { category: "RU", base: "19.3.0.0.0", release: "19.28.0.0.250715", patchnum: "37952382", patchfile: "p37952382_190000_Linux-x86-64.zip", patch_subdir: "/37957391", prereq_check: FALSE, method: "opatchauto apply", ocm: FALSE, upgrade: FALSE, md5sum: "GL89d7Hnfe+fG0kVCgcwSw==" } - - # 21c GRID RU - 21.4 and 21.7 are available only via SR # RDBMS RU - 21.4 - 21.7 are available only via SR # so 21.5 and 21.6 are commented out here, even though available
{category}{software_piece}{files}
{category}{software_piece}{files}
{category}{software_piece}{files}
{category}{software_piece}{files}