Repository: opensearch-project/opensearch-build

Issue:
[Bug]: Release notes checker does not check release notes for core engines
### Describe the bug

With recent enhancement to compile consolidated release notes [PR](https://github.com/opensearch-project/opensearch-build/pull/4196), a change was introduced to skip few components [here](https://github.com/opensearch-project/opensearch-build/pull/4196/files#diff-367096ff0c801397154d58cb4c8fea65125f353b543f5bd686f55bf187499afc)

However, we do not add core engine (OpenSearch and OpenSearch-Dashboards) release notes, we do however need to check their presence in release notes tracker workflow that posts the comments on the release issue. 
Example: https://github.com/opensearch-project/opensearch-build/issues/4433#issuecomment-2004998714


### To reproduce

See https://github.com/opensearch-project/opensearch-build/issues/4433#issuecomment-2004998714

### Expected behavior

The table should also component: OpenSearch and OpenSearch-Dashboards. 

The condition is true for `action_type: compile` but the`action_type: check` should only skip `notifications-core` and not others

### Screenshots

If applicable, add screenshots to help explain your problem.

### Host / Environment

_No response_

### Additional context

_No response_

### Relevant log output

_No response_


Discussion:
Will take a look. 
One more thing, 
There is another bug let me know if I should create a different issue for the same.
The consolidated release notes adds `ml-commons` as `ml-common` and `Opensearch` should be `OpenSearch` in all headings. Somehow this is getting messed up. Example: https://github.com/opensearch-project/opensearch-build/pull/4595/commits/7b2352ecc0c7ee486f461e201ca2d131abc4ac5b

Resolving patch:
diff --git a/src/release_notes_workflow/release_notes.py b/src/release_notes_workflow/release_notes.py
--- a/src/release_notes_workflow/release_notes.py
+++ b/src/release_notes_workflow/release_notes.py
@@ -19,18 +19,19 @@
 
 class ReleaseNotes:
 
-    def __init__(self, manifest: InputManifest, date: str, action_type: str) -> None:
-        self.manifest = manifest
+    def __init__(self, input_manifests: List[InputManifest], date: str, action_type: str) -> None:
+        self.manifests = input_manifests  # type: ignore[assignment]
         self.date = date
         self.action_type = action_type
 
     def table(self) -> MarkdownTableWriter:
         table_result = []
-        for component in self.manifest.components.select():
-            if component.name == 'OpenSearch' or component.name == 'OpenSearch-Dashboards' or component.name == 'notifications-core':
-                continue
-            if hasattr(component, "repository"):
-                table_result.append(self.check(component))  # type: ignore[arg-type]
+        for manifest in self.manifests:
+            for component in manifest.components.select():
+                if (component.name == 'OpenSearch' or component.name == 'OpenSearch-Dashboards' or component.name == 'notifications-core') and self.action_type == 'compile':
+                    continue
+                if hasattr(component, "repository"):
+                    table_result.append(self.check(component, manifest.build.version))  # type: ignore[arg-type]
 
         # Sort table_result based on Repo column
         table_result.sort(key=lambda x: (x[0], x[1]) if len(x) > 1 else x[0])
@@ -43,13 +44,13 @@ def table(self) -> MarkdownTableWriter:
             raise ValueError("Invalid action_type. Use 'check' or 'compile'.")
 
         writer = MarkdownTableWriter(
-            table_name=f" {self.manifest.build.name} CommitID(after {self.date}) & Release Notes info",
+            table_name=f"Core Components CommitID(after {self.date}) & Release Notes info",
             headers=headers,
             value_matrix=table_result
         )
         return writer
 
-    def check(self, component: InputComponentFromSource) -> List:
+    def check(self, component: InputComponentFromSource, build_version: str) -> List:
         results = []
         with TemporaryDirectory(chdir=True) as work_dir:
             results.append(component.name)
@@ -61,7 +62,7 @@ def check(self, component: InputComponentFromSource) -> List:
                     component.working_directory,
             ) as repo:
                 logging.debug(f"Checked out {component.name} into {repo.dir}")
-                release_notes = ReleaseNotesComponents.from_component(component, self.manifest.build.version, repo.dir)
+                release_notes = ReleaseNotesComponents.from_component(component, build_version, repo.dir)
                 commits = repo.log(self.date)
                 if len(commits) > 0:
                     last_commit = commits[-1]
diff --git a/src/release_notes_workflow/release_notes_check_args.py b/src/release_notes_workflow/release_notes_check_args.py
--- a/src/release_notes_workflow/release_notes_check_args.py
+++ b/src/release_notes_workflow/release_notes_check_args.py
@@ -8,19 +8,19 @@
 import argparse
 import datetime
 import logging
-from typing import IO
+from typing import IO, List
 
 
 class ReleaseNotesCheckArgs:
     action: str
-    manifest: IO
+    manifest: List[IO]
     date: str
     output: str
 
     def __init__(self) -> None:
         parser = argparse.ArgumentParser(description="Checkout an OpenSearch Bundle and check for CommitID and Release Notes")
         parser.add_argument("action", choices=["check", "compile"], help="Operation to perform.")
-        parser.add_argument("manifest", type=argparse.FileType("r"), help="Manifest file.")
+        parser.add_argument("manifest", type=argparse.FileType("r"), nargs='+', help="Manifest file.")
         parser.add_argument(
             "-v",
             "--verbose",
diff --git a/src/run_releasenotes_check.py b/src/run_releasenotes_check.py
--- a/src/run_releasenotes_check.py
+++ b/src/run_releasenotes_check.py
@@ -10,7 +10,9 @@
 import re
 import shutil
 from collections import defaultdict
+from typing import List
 
+import markdownify
 import mistune
 import requests
 
@@ -23,18 +25,41 @@
 def main() -> int:
     args = ReleaseNotesCheckArgs()
     console.configure(level=args.logging_level)
-    manifest_file = InputManifest.from_file(args.manifest)
-    BUILD_VERSION = manifest_file.build.version
-
+    manifests: List[InputManifest] = []
     # storing temporary release notes for testing purposes
     BASE_FILE_PATH = "release_notes_workflow/results"
+
+    for input_manifests in args.manifest:
+        manifests.append(InputManifest.from_file(input_manifests))
+
+    if len(args.manifest) == 2:
+        if manifests[0].build.version != manifests[1].build.version:
+            raise ValueError("OS and OSD manifests must be provided for the same release version")
+        elif manifests[0].build.name == manifests[1].build.name:
+            raise ValueError("Both manifests are for the same product, OS and OSD manifests must be provided")
+    if len(args.manifest) > 2:
+        raise ValueError("Only two manifests, OS and OSD, can be provided")
+
+    #  Assuming that the OS and OSD manifests will be provided for the same release version
+    BUILD_VERSION = manifests[0].build.version
+
     table_filename = f"{BASE_FILE_PATH}/release_notes_table-{BUILD_VERSION}.md"
     urls_filename = f"{BASE_FILE_PATH}/release_notes_urls-{BUILD_VERSION}.txt"
 
+    def check_if_exists_then_delete(file_path: List[str]) -> None:
+        for file in file_path:
+            if os.path.exists(os.path.join(os.path.dirname(__file__), file)):
+                print(f"file {file} exists. Deleting")
+                os.remove(os.path.join(os.path.dirname(__file__), file))
+            else:
+                logging.info(f"The file {file} does not exist, creating new.")
+
     def capitalize_acronyms(formatted_name: str) -> str:
-        acronyms = {"sql": "SQL", "ml": "ML", "knn": "k-NN", "k-nn": "k-NN", "ml-commons": "ML Commons", "ml commons": "ML Commons"}
+        acronyms = {"sql": "SQL", "ml": "ML", "knn": "k-NN", "k-nn": "k-NN", "ml-commons": "ML Commons",
+                    "ml commons": "ML Commons"}
         for acronym, replacement in acronyms.items():
-            formatted_name = re.sub(r'\b' + re.escape(acronym) + r'\b', replacement, formatted_name, flags=re.IGNORECASE)
+            formatted_name = re.sub(r'\b' + re.escape(acronym) + r'\b', replacement, formatted_name,
+                                    flags=re.IGNORECASE)
         return formatted_name
 
     def format_component_name_from_url(url: str) -> str:
@@ -50,14 +75,14 @@ def format_component_name_from_url(url: str) -> str:
         formatted_name = " ".join(word.capitalize() for word in re.split(r"[-.]", component_name))
         return capitalize_acronyms(formatted_name)
 
-    def create_urls_file_if_not_exists() -> None:
+    def create_urls_file_if_not_exists(manifest_files: List[InputManifest]) -> None:
 
-        release_notes = ReleaseNotes(manifest_file, args.date, args.action)
+        release_notes = ReleaseNotes(manifest_files, args.date, args.action)
         table = release_notes.table()
 
         table_filepath = os.path.join(os.path.dirname(__file__), table_filename)
         os.makedirs(os.path.dirname(table_filepath), exist_ok=True)
-        with open(table_filepath, "w") as table_file:
+        with open(table_filepath, "a") as table_file:
             table.dump(table_file)
 
         if args.output is not None:
@@ -71,15 +96,17 @@ def create_urls_file_if_not_exists() -> None:
 
         urls_filepath = os.path.join(os.path.dirname(__file__), urls_filename)
         os.makedirs(os.path.dirname(urls_filepath), exist_ok=True)
-        with open(urls_filepath, "w") as urls_file:
+        with open(urls_filepath, "a") as urls_file:
             urls_file.writelines("\n".join(urls))
 
     if args.action == "check":
-        create_urls_file_if_not_exists()
+        check_if_exists_then_delete([table_filename, urls_filename])
+        create_urls_file_if_not_exists(manifests)
         return 0
 
     elif args.action == "compile":
-        create_urls_file_if_not_exists()
+        check_if_exists_then_delete([table_filename, urls_filename])
+        create_urls_file_if_not_exists(manifests)
 
         RELEASENOTES_CATEGORIES = "BREAKING,FEATURES,ENHANCEMENTS,BUG FIXES,INFRASTRUCTURE,DOCUMENTATION,MAINTENANCE,REFACTORING,EXPERIMENTAL"
         RELEASE_NOTE_MD = f"{BASE_FILE_PATH}/release_notes-{BUILD_VERSION}.md"
@@ -148,11 +175,11 @@ def create_urls_file_if_not_exists() -> None:
     # Markdown renderer
     markdown = mistune.create_markdown()
 
-    RELEASE_NOTE_MD_path = os.path.join(os.path.dirname(__file__), RELEASE_NOTE_MD)
-    os.makedirs(os.path.dirname(RELEASE_NOTE_MD_path), exist_ok=True)
+    RELEASE_NOTE_MD_PATH = os.path.join(os.path.dirname(__file__), RELEASE_NOTE_MD)
+    os.makedirs(os.path.dirname(RELEASE_NOTE_MD_PATH), exist_ok=True)
 
     # Filter content for each category
-    with open(RELEASE_NOTE_MD_path, "w") as outfile:
+    with open(RELEASE_NOTE_MD_PATH, "w") as outfile:
         outfile.write(markdown(f"# OpenSearch and OpenSearch Dashboards {BUILD_VERSION} Release Notes\n\n"))
 
         for category in RELEASENOTES_CATEGORIES.split(","):
@@ -188,11 +215,19 @@ def create_urls_file_if_not_exists() -> None:
             for item in temp_content:
                 outfile.write(markdown(item))
 
+    with open(RELEASE_NOTE_MD_PATH, 'r') as f:
+        html_content = f.read()
+
+    markdown_content = markdownify.markdownify(html_content, heading_style="ATX")
+
+    with open(RELEASE_NOTE_MD_PATH, 'w') as f:
+        f.write(markdown_content)
+
     if args.output is not None:
         logging.info(f"Moving {RELEASE_NOTE_MD} to {args.output}")
-        shutil.move(RELEASE_NOTE_MD_path, args.output)
+        shutil.move(RELEASE_NOTE_MD_PATH, args.output)
     else:
-        logging.info(f"Release notes compiled to {RELEASE_NOTE_MD_path}")
+        logging.info(f"Release notes compiled to {RELEASE_NOTE_MD_PATH}")
     return 0
 
 


Test patch:
diff --git a/tests/data/opensearch-dashboards-test-main.yml b/tests/data/opensearch-dashboards-test-main.yml
new file mode 100644
--- /dev/null
+++ b/tests/data/opensearch-dashboards-test-main.yml
@@ -0,0 +1,16 @@
+---
+schema-version: '1.1'
+build:
+  name: OpenSearch Dashboards
+  version: '1.0'
+ci:
+  image:
+    name: opensearchstaging/ci-runner:ci-runner-centos7-opensearch-build-v3
+    args: -e JAVA_HOME=/opt/java/openjdk-17
+components:
+  - name: OpenSearch-Dashboards-test
+    repository: https://github.com/opensearch-project/OpenSearch.git
+    ref: main
+    checks:
+      - gradle:publish
+      - gradle:properties:version
diff --git a/tests/data/opensearch-test-main-2.0.yml b/tests/data/opensearch-test-main-2.0.yml
new file mode 100644
--- /dev/null
+++ b/tests/data/opensearch-test-main-2.0.yml
@@ -0,0 +1,16 @@
+---
+schema-version: '1.1'
+build:
+  name: OpenSearch
+  version: '2.0'
+ci:
+  image:
+    name: opensearchstaging/ci-runner:ci-runner-centos7-opensearch-build-v3
+    args: -e JAVA_HOME=/opt/java/openjdk-17
+components:
+  - name: OpenSearch-test
+    repository: https://github.com/opensearch-project/OpenSearch.git
+    ref: main
+    checks:
+      - gradle:publish
+      - gradle:properties:version
diff --git a/tests/data/opensearch-test-main.yml b/tests/data/opensearch-test-main.yml
--- a/tests/data/opensearch-test-main.yml
+++ b/tests/data/opensearch-test-main.yml
@@ -1,8 +1,8 @@
 ---
-schema-version: '1.0'
+schema-version: '1.1'
 build:
   name: OpenSearch
-  version: 'replace'
+  version: '1.0'
 ci:
   image:
     name: opensearchstaging/ci-runner:ci-runner-centos7-opensearch-build-v3
diff --git a/tests/tests_release_notes_workflow/test_release_note_multiple_manifest.py b/tests/tests_release_notes_workflow/test_release_note_multiple_manifest.py
new file mode 100644
--- /dev/null
+++ b/tests/tests_release_notes_workflow/test_release_note_multiple_manifest.py
@@ -0,0 +1,58 @@
+# Copyright OpenSearch Contributors
+# SPDX-License-Identifier: Apache-2.0
+#
+# The OpenSearch Contributors require contributions made to
+# this file be licensed under the Apache-2.0 license or a
+# compatible open source license.
+
+import os
+import unittest
+from typing import Any
+from unittest.mock import patch
+
+from manifests.input_manifest import InputComponentFromSource, InputManifest
+from release_notes_workflow.release_notes import ReleaseNotes
+
+
+class TestReleaseNotes(unittest.TestCase):
+    MANIFESTS = os.path.join(
+        os.path.dirname(__file__),
+        "..",
+        "data",
+    )
+
+    def setUp(self) -> None:
+        OPENSEARCH_MANIFEST = os.path.realpath(os.path.join(TestReleaseNotes.MANIFESTS, "opensearch-test-main.yml"))
+        DASHBOARDS_MANIFEST = os.path.realpath(
+            os.path.join(TestReleaseNotes.MANIFESTS, "opensearch-dashboards-test-main.yml"))
+        self.opensearch_manifest = InputManifest.from_file(open(OPENSEARCH_MANIFEST))
+        self.dashboards_manifest = InputManifest.from_file(open(DASHBOARDS_MANIFEST))
+        self.build_version = self.opensearch_manifest.build.version
+        self.release_notes = ReleaseNotes([self.opensearch_manifest, self.dashboards_manifest], "2022-07-26", "compile")
+        self.opensearch_component = InputComponentFromSource(
+            {"name": "OpenSearch-test", "repository": "url", "ref": "ref"})
+        self.dashboards_component = InputComponentFromSource(
+            {"name": "OpenSearch-Dashboards-test", "repository": "url", "ref": "ref"})
+
+    @patch("subprocess.check_output", return_value=''.encode())
+    @patch("subprocess.check_call")
+    def test_check(self, *mocks: Any) -> None:
+        self.assertEqual(self.release_notes.check(self.opensearch_component, self.build_version),
+                         ['OpenSearch-test', '[ref]', None, None, False, None])
+        self.assertEqual(self.release_notes.check(self.dashboards_component, self.build_version),
+                         ['OpenSearch-Dashboards-test', '[ref]', None, None, False, None])
+
+    @patch("subprocess.check_output", return_value=''.encode())
+    @patch("subprocess.check_call")
+    def test_table(self, *mocks: Any) -> None:
+        self.release_notes.check(self.opensearch_component, self.build_version)
+        self.release_notes.check(self.dashboards_component, self.build_version)
+        self.release_notes.table()
+
+        self.assertEqual(self.release_notes.table()._table_name.strip(),
+                         'Core Components CommitID(after 2022-07-26) & Release Notes info')
+        self.assertEqual(self.release_notes.table().headers,
+                         ['Repo', 'Branch', 'CommitID', 'Commit Date', 'Release Notes Exists', 'URL'])
+        self.assertEqual(self.release_notes.table().value_matrix,
+                         [['OpenSearch-Dashboards-test', '[main]', None, None, False, None],
+                          ['OpenSearch-test', '[main]', None, None, False, None]])
diff --git a/tests/tests_release_notes_workflow/test_release_notes.py b/tests/tests_release_notes_workflow/test_release_notes.py
--- a/tests/tests_release_notes_workflow/test_release_notes.py
+++ b/tests/tests_release_notes_workflow/test_release_notes.py
@@ -24,13 +24,14 @@ def setUp(self) -> None:
         )
         OPENSEARCH_MANIFEST = os.path.realpath(os.path.join(MANIFESTS, "opensearch-test-main.yml"))
         self.manifest_file = InputManifest.from_file(open(OPENSEARCH_MANIFEST))
-        self.release_notes = ReleaseNotes(self.manifest_file, "2022-07-26", "compile")
+        self.build_version = self.manifest_file.build.version
+        self.release_notes = ReleaseNotes([self.manifest_file], "2022-07-26", "compile")
         self.component = InputComponentFromSource({"name": "OpenSearch-test", "repository": "url", "ref": "ref"})
 
     @patch("subprocess.check_output", return_value=''.encode())
     @patch("subprocess.check_call")
     def test_check(self, *mocks: Any) -> None:
-        self.assertEqual(self.release_notes.check(self.component), ['OpenSearch-test', '[ref]', None, None, False, None])
+        self.assertEqual(self.release_notes.check(self.component, self.build_version), ['OpenSearch-test', '[ref]', None, None, False, None])
 
     @patch("subprocess.check_output", return_value=''.encode())
     @patch("subprocess.check_call")
@@ -38,11 +39,11 @@ def test_check_with_manifest(self, *mocks: Any) -> None:
         component = next(self.manifest_file.components.select())
         if type(component) is InputComponentFromSource:
             self.assertIsInstance(component, InputComponentFromSource)
-            self.assertEqual(self.release_notes.check(component), ['OpenSearch-test', '[main]', None, None, False])
+            self.assertEqual(self.release_notes.check(component, self.build_version), ['OpenSearch-test', '[main]', None, None, False, None])
 
-    @patch('release_notes_workflow.release_notes.ReleaseNotes.check', return_value=['OpenSearch-test', '[main]', 'ee26e01', '2022-08-18', False])
+    @patch('release_notes_workflow.release_notes.ReleaseNotes.check', return_value=['OpenSearch-test', '[main]', 'ee26e01', '2022-08-18', False, None])
     def test_table(self, *mocks: Any) -> None:
         table_output = self.release_notes.table()
-        self.assertEqual(table_output._table_name.strip(), 'OpenSearch CommitID(after 2022-07-26) & Release Notes info')
+        self.assertEqual(table_output._table_name.strip(), 'Core Components CommitID(after 2022-07-26) & Release Notes info')
         self.assertEqual(table_output.headers, ['Repo', 'Branch', 'CommitID', 'Commit Date', 'Release Notes Exists', 'URL'])
-        self.assertEqual(table_output.value_matrix, [['OpenSearch-test', '[main]', 'ee26e01', '2022-08-18', False]])
+        self.assertEqual(table_output.value_matrix, [['OpenSearch-test', '[main]', 'ee26e01', '2022-08-18', False, None]])
diff --git a/tests/tests_release_notes_workflow/test_releasenotes_check_args.py b/tests/tests_release_notes_workflow/test_releasenotes_check_args.py
--- a/tests/tests_release_notes_workflow/test_releasenotes_check_args.py
+++ b/tests/tests_release_notes_workflow/test_releasenotes_check_args.py
@@ -15,6 +15,7 @@
 import pytest
 
 from release_notes_workflow.release_notes_check_args import ReleaseNotesCheckArgs
+from run_releasenotes_check import main
 
 
 class TestReleaseNotesCheckArgs(unittest.TestCase):
@@ -34,18 +35,45 @@ class TestReleaseNotesCheckArgs(unittest.TestCase):
         )
     )
 
+    DASHBOARDS_MANIFEST = os.path.realpath(
+        os.path.join(
+            os.path.dirname(__file__),
+            "..",
+            "..",
+            "manifests",
+            "templates",
+            "opensearch-dashboards",
+            "1.x",
+            "osd-template-1.1.0.yml",
+        )
+    )
+
+    DASHBOARDS_MANIFEST_2_0 = os.path.realpath(
+        os.path.join(
+            os.path.dirname(__file__),
+            "..",
+            "..",
+            "manifests",
+            "templates",
+            "opensearch-dashboards",
+            "2.x",
+            "osd-template-2.0.0.yml",
+        )
+    )
+
     @pytest.fixture(autouse=True)
     def _capfd(self, capfd: Any) -> None:
         self.capfd = capfd
 
     @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "check", OPENSEARCH_MANIFEST, "--date", '2022-07-26'])
     def test_manifest(self) -> None:
-        self.assertEqual(ReleaseNotesCheckArgs().manifest.name, TestReleaseNotesCheckArgs.OPENSEARCH_MANIFEST)
+        self.assertEqual(ReleaseNotesCheckArgs().manifest[0].name, TestReleaseNotesCheckArgs.OPENSEARCH_MANIFEST)
         self.assertEqual(ReleaseNotesCheckArgs().date, datetime.date(2022, 7, 26))
 
-    @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "compile", OPENSEARCH_MANIFEST, "--date", '2022-07-26'])
+    @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "compile", OPENSEARCH_MANIFEST, DASHBOARDS_MANIFEST, "--date", '2022-07-26'])
     def test_manifest_compile(self) -> None:
-        self.assertEqual(ReleaseNotesCheckArgs().manifest.name, TestReleaseNotesCheckArgs.OPENSEARCH_MANIFEST)
+        self.assertEqual(ReleaseNotesCheckArgs().manifest[0].name, TestReleaseNotesCheckArgs.OPENSEARCH_MANIFEST)
+        self.assertEqual(ReleaseNotesCheckArgs().manifest[1].name, TestReleaseNotesCheckArgs.DASHBOARDS_MANIFEST)
         self.assertEqual(ReleaseNotesCheckArgs().date, datetime.date(2022, 7, 26))
 
     @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "check", OPENSEARCH_MANIFEST])
@@ -71,3 +99,21 @@ def test_output(self) -> None:
     @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "compile", OPENSEARCH_MANIFEST, "--date", '2022-07-26', "--output", "test.md"])
     def test_output_compile(self) -> None:
         self.assertEqual(ReleaseNotesCheckArgs().output, "test.md")
+
+    @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "compile", OPENSEARCH_MANIFEST, DASHBOARDS_MANIFEST_2_0])
+    def test_error_on_different_release_version_manifest(self) -> None:
+        with pytest.raises(ValueError) as exc_info:
+            main()
+        assert str(exc_info.value) == 'OS and OSD manifests must be provided for the same release version'
+
+    @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "compile", OPENSEARCH_MANIFEST, OPENSEARCH_MANIFEST])
+    def test_error_on_same_manifest_product_name(self) -> None:
+        with pytest.raises(ValueError) as exc_info:
+            main()
+        assert str(exc_info.value) == 'Both manifests are for the same product, OS and OSD manifests must be provided'
+
+    @patch("argparse._sys.argv", [RELEASE_NOTES_CHECK_PY, "compile", OPENSEARCH_MANIFEST, OPENSEARCH_MANIFEST, OPENSEARCH_MANIFEST])
+    def test_error_on_more_than_two_manifests(self, *mocks: Any) -> None:
+        with pytest.raises(ValueError) as exc_info:
+            main()
+        assert str(exc_info.value) == 'Only two manifests, OS and OSD, can be provided'