Repository: opensearch-project/opensearch-build

Issue:
Check versions of artifacts bundled in a distribution
**Is your feature request related to a problem? Please describe.**
Bundle being built for a release do not verify the versions of the artifacts or that "everything works together".
Artifacts name them version x but can still be of version y and built out of version z of OpenSearch/Dashboards 
**Describe the solution you'd like**
We'd like to improve the release quality. We should an automated way to verify the artifacts provided with the version they claim to be and the version we'd like to build for release.

Whats the motivation behind this?
Anomaly Detection plugin was still using OpenSearch and its dependencies with 1.0.0-rc1 and claiming the plugin version to be 1.0.0.0. But really underneath they were running different versions and cannot be bundled for a release as the plugin should have been built with 1.0.0 of OpenSearch and 1.0.0.0 of other dependent plugins/libraries.  
Ref: 
AD 1.0 release branch: https://github.com/opensearch-project/anomaly-detection/blob/1.0.0.0/build.gradle#L33-L35
Issue to fix after the release is done: https://github.com/opensearch-project/anomaly-detection/issues/153

We cannot manually scale and should have an automated workflow to check these artifacts for every release to start with.
With the CI infrastructure we are building, we should be able to add another CI job to verify the artifacts just before bundling them for the release.


Discussion:
https://github.com/opensearch-project/anomaly-detection/issues/153
Seems we forgot to backport https://github.com/opensearch-project/anomaly-detection/pull/114 to 1.0 branch. Any suggestion about how to avoid such error in future? @saratvemulapalli @dblock 
Any manual process involved it can lead to errors. 
We should build tooling around this which is what I have explained above to have a job while building a bundle to verify it. 
Do you have any other suggestions?
These dependencies should be strong, aka if anomaly-detection was built against OpenSearch x.y, it should not run unless the OpenSearch JAR it's loading matches x.y. For 1.0 we relied on a branch, which is definitely not scalable.
> Any manual process involved it can lead to errors.
> We should build tooling around this which is what I have explained above to have a job while building a bundle to verify it.
> Do you have any other suggestions?

We already have steps to build AD bundle in AD CI: https://github.com/opensearch-project/anomaly-detection/blob/1.0/.github/workflows/CI.yml#L78. But the CI didn't fail as we use rc1 at all places in 1.0 branch.
Maybe infra team can add same build steps in their CI workflow to make sure all plugins have correct version. 

Resolving patch:
diff --git a/bundle-workflow/src/build_workflow/build_recorder.py b/bundle-workflow/src/build_workflow/build_recorder.py
--- a/bundle-workflow/src/build_workflow/build_recorder.py
+++ b/bundle-workflow/src/build_workflow/build_recorder.py
@@ -6,22 +6,84 @@
 
 import os
 import shutil
+from zipfile import ZipFile
 
 import yaml
+from jproperties import Properties  # type: ignore
 
 from manifests.build_manifest import BuildManifest
 
 
 class BuildRecorder:
+    class ArtifactInvalidError(Exception):
+        def __init__(self, path, message):
+            self.path = path
+            super().__init__(
+                f"Artifact {os.path.basename(path)} is invalid: {message}."
+            )
+
+    class PropertiesFile(Properties):
+        def __init__(self, filename, data):
+            super().__init__(self)
+            self.filename = filename
+            self.load(data)
+
+        def get_value(self, key, default_value=None):
+            try:
+                return self[key].data
+            except KeyError:
+                return default_value
+
+        def check_value(self, key, expected_value):
+            try:
+                value = self[key].data
+                if value != expected_value:
+                    raise BuildRecorder.ArtifactInvalidError(
+                        self.filename,
+                        f"expected to have {key}={expected_value}, but was {value}",
+                    )
+            except KeyError:
+                raise BuildRecorder.ArtifactInvalidError(
+                    self.filename,
+                    f"expected to have {key}={expected_value}, but none was found",
+                )
+
+        def check_value_in(self, key, expected_values):
+            try:
+                value = self[key].data
+                if value not in expected_values:
+                    raise BuildRecorder.ArtifactInvalidError(
+                        self.filename,
+                        f"expected to have {key}=any of {expected_values}, but was {value}",
+                    )
+            except KeyError:
+                if None not in expected_values:
+                    raise BuildRecorder.ArtifactInvalidError(
+                        self.filename,
+                        f"expected to have {key}=any of {expected_values}, but none was found",
+                    )
+
     def __init__(self, build_id, output_dir, name, version, arch, snapshot):
         self.output_dir = output_dir
+        self.version = str(version)
+        self.opensearch_version = (
+            self.version + "-SNAPSHOT" if snapshot else self.version
+        )
+        # BUG: the 4th digit is dictated by the component, it's not .0, this will break for 1.1.0.1
+        self.component_version = (
+            self.version + ".0-SNAPSHOT" if snapshot else f"{self.version}.0"
+        )
         self.build_manifest = self.BuildManifestBuilder(
-            build_id, name, version, arch, snapshot
+            build_id, name, self.opensearch_version, arch, snapshot
         )
 
     def record_component(self, component_name, git_repo):
         self.build_manifest.append_component(
-            component_name, git_repo.url, git_repo.ref, git_repo.sha
+            component_name,
+            self.component_version,
+            git_repo.url,
+            git_repo.ref,
+            git_repo.sha,
         )
 
     def record_artifact(
@@ -34,6 +96,8 @@ def record_artifact(
         dest_file = os.path.join(self.output_dir, artifact_path)
         dest_dir = os.path.dirname(dest_file)
         os.makedirs(dest_dir, exist_ok=True)
+        # Check artifact
+        self.__check_artifact(artifact_type, artifact_file)
         # Copy the file
         shutil.copyfile(artifact_file, dest_file)
         # Notify the recorder
@@ -50,15 +114,64 @@ def write_manifest(self, dest_dir):
         with open(manifest_path, "w") as file:
             yaml.dump(output_manifest.to_dict(), file)
 
+    def __check_artifact(self, artifact_type, artifact_file):
+        if artifact_type == "plugins":
+            self.__check_plugin_artifact(artifact_file)
+        elif artifact_type == "maven":
+            self.__check_maven_artifact(artifact_file)
+
+    def __check_plugin_artifact(self, artifact_file):
+        if os.path.splitext(artifact_file)[1] != ".zip":
+            raise BuildRecorder.ArtifactInvalidError(artifact_file, "not a zip file")
+        if not artifact_file.endswith(f"-{self.component_version}.zip"):
+            raise BuildRecorder.ArtifactInvalidError(
+                artifact_file, f"expected filename to include {self.component_version}"
+            )
+        with ZipFile(artifact_file, "r") as zip:
+            data = zip.read("plugin-descriptor.properties").decode("UTF-8")
+            properties = BuildRecorder.PropertiesFile(artifact_file, data)
+            properties.check_value("version", self.component_version)
+            properties.check_value("opensearch.version", self.version)
+            print(f'Checked {artifact_file} ({properties.get_value("version", "N/A")})')
+
+    def __check_maven_artifact(self, artifact_file):
+        ext = os.path.splitext(artifact_file)[1]
+        if ext not in [
+            ".jar",
+            ".asc",
+            ".md5",
+            ".sha1",
+            ".pom",
+            ".xml",
+            ".sha1",
+            ".sha256",
+            ".sha512",
+            ".module",
+            ".zip",
+            ".war",
+        ]:
+            raise BuildRecorder.ArtifactInvalidError(
+                artifact_file, f"{ext} is not a valid extension for a maven file"
+            )
+        if os.path.splitext(artifact_file)[1] == ".jar":
+            with ZipFile(artifact_file, "r") as zip:
+                data = zip.read("META-INF/MANIFEST.MF").decode("UTF-8")
+                properties = BuildRecorder.PropertiesFile(artifact_file, data)
+                properties.check_value_in(
+                    "Implementation-Version",
+                    [self.component_version, self.opensearch_version, None],
+                )
+                print(
+                    f'Checked {artifact_file} ({properties.get_value("Implementation-Version", "N/A")})'
+                )
+
     class BuildManifestBuilder:
         def __init__(self, build_id, name, version, arch, snapshot):
             self.data = {}
             self.data["build"] = {}
             self.data["build"]["id"] = build_id
             self.data["build"]["name"] = name
-            self.data["build"]["version"] = (
-                str(version) + "-SNAPSHOT" if snapshot else str(version)
-            )
+            self.data["build"]["version"] = version
             self.data["build"]["architecture"] = arch
             self.data["build"]["snapshot"] = str(snapshot).lower()
             self.data["schema-version"] = "1.0"
@@ -66,13 +179,14 @@ def __init__(self, build_id, name, version, arch, snapshot):
             # When we convert to a BuildManifest this will get converted back into a list
             self.data["components_hash"] = {}
 
-        def append_component(self, name, repository_url, ref, commit_id):
+        def append_component(self, name, version, repository_url, ref, commit_id):
             component = {
                 "name": name,
                 "repository": repository_url,
                 "ref": ref,
                 "commit_id": commit_id,
                 "artifacts": {},
+                "version": version,
             }
             self.data["components_hash"][name] = component
 
diff --git a/bundle-workflow/src/manifests/build_manifest.py b/bundle-workflow/src/manifests/build_manifest.py
--- a/bundle-workflow/src/manifests/build_manifest.py
+++ b/bundle-workflow/src/manifests/build_manifest.py
@@ -76,6 +76,7 @@ def __init__(self, data):
             self.ref = data["ref"]
             self.commit_id = data["commit_id"]
             self.artifacts = data["artifacts"]
+            self.version = data["version"]
 
         def to_dict(self):
             return {
@@ -84,4 +85,5 @@ def to_dict(self):
                 "ref": self.ref,
                 "commit_id": self.commit_id,
                 "artifacts": self.artifacts,
+                "version": self.version,
             }


Test patch:
diff --git a/bundle-workflow/tests/tests_assemble_workflow/data/opensearch-build-1.1.0.yml b/bundle-workflow/tests/tests_assemble_workflow/data/opensearch-build-1.1.0.yml
--- a/bundle-workflow/tests/tests_assemble_workflow/data/opensearch-build-1.1.0.yml
+++ b/bundle-workflow/tests/tests_assemble_workflow/data/opensearch-build-1.1.0.yml
@@ -1,6 +1,6 @@
 build:
   architecture: x64
-  id: f39b2d68f8b74389a0037f3e8d6ba0b0
+  id: c3ff7a232d25403fa8cc14c97799c323
   name: OpenSearch
   version: 1.1.0
 components:
@@ -2329,10 +2329,11 @@ components:
     - maven/org/opensearch/script-expert-scoring/1.1.0/script-expert-scoring-1.1.0.jar.sha256
     - maven/org/opensearch/script-expert-scoring/1.1.0/script-expert-scoring-1.1.0.jar
     - maven/org/opensearch/script-expert-scoring/1.1.0/script-expert-scoring-1.1.0.module.sha256
-  commit_id: 07a57d9bbb3922079b7bb1be83a01252f57f81ec
+  commit_id: b7334f49d530ffd1a3f7bd0e5832b9b2a9caa583
   name: OpenSearch
   ref: 1.x
   repository: https://github.com/opensearch-project/OpenSearch.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/common-utils/maven-metadata-local.xml
@@ -2344,6 +2345,7 @@ components:
   name: common-utils
   ref: main
   repository: https://github.com/opensearch-project/common-utils.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/opensearch-job-scheduler/maven-metadata-local.xml
@@ -2363,34 +2365,45 @@ components:
   name: job-scheduler
   ref: main
   repository: https://github.com/opensearch-project/job-scheduler.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-sql-1.1.0.0.zip
-  commit_id: 9ada74383a812baf534af8984a276303e4619574
+  commit_id: d68547d585092af1e053d01e1b834259723cd304
   name: sql
   ref: main
   repository: https://github.com/opensearch-project/sql.git
+  version: 1.1.0.0
 - artifacts:
+    maven:
+    - maven/org/opensearch/notification/maven-metadata-local.xml
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0-sources.jar
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0.pom
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0.jar
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0-javadoc.jar
     plugins:
     - plugins/opensearch-alerting-1.1.0.0.zip
-  commit_id: ecd283f5c7eac51902a43ef577e44fdccdc0e5ae
+  commit_id: 8024b8b9195f837e49e5bebd7f4a31dfc333eb4d
   name: alerting
   ref: main
   repository: https://github.com/opensearch-project/alerting.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-notifications-1.1.0.0.zip
-  commit_id: 7efc43a0a2f6829b170909ede56d70434d32ab1d
+  commit_id: d0d3e485c4a850f73652a989eeec795b7347fbb6
   name: notifications
   ref: main
   repository: https://github.com/opensearch-project/notifications.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-security-1.1.0.0.zip
-  commit_id: 25383effb9b7eeb3718e1fd7288e388a074568bf
+  commit_id: 534fffe0e6cf2b33b9abcbc6508e98fc2d077a3d
   name: security
-  ref: fix-snapshot-build
-  repository: https://github.com/dblock/security.git
+  ref: main
+  repository: https://github.com/opensearch-project/security.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/performanceanalyzer-rca/maven-metadata-local.xml
@@ -2401,6 +2414,7 @@ components:
   name: performance-analyzer-rca
   ref: main
   repository: https://github.com/opensearch-project/performance-analyzer-rca.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/opensearch-performance-analyzer/maven-metadata-local.xml
@@ -2411,17 +2425,19 @@ components:
     - maven/org/opensearch/opensearch-performance-analyzer/1.1.0.0/opensearch-performance-analyzer-1.1.0.0-sources.jar
     plugins:
     - plugins/opensearch-performance-analyzer-1.1.0.0.zip
-  commit_id: 95c2b6c7054f3e52df5d31d7859515b19bc95b59
+  commit_id: f184f0bc39302ac38af2585c663d619048f6cffe
   name: performance-analyzer
   ref: main
   repository: https://github.com/opensearch-project/performance-analyzer.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-index-management-1.1.0.0.zip
-  commit_id: ec0578934bae1fdee31ead07500ae8cee633c6d7
+  commit_id: 7897e9ae9cd5b49535e6a8bbf4c2f73cb458af24
   name: index-management
   ref: main
   repository: https://github.com/opensearch-project/index-management.git
+  version: 1.1.0.0
 - artifacts:
     libs:
     - libs/libKNNIndexV2_0_11.so
@@ -2431,13 +2447,15 @@ components:
   name: k-NN
   ref: main
   repository: https://github.com/opensearch-project/k-NN.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-anomaly-detection-1.1.0.0.zip
-  commit_id: b5d02ebfffab35a3918c3b603da328584497b589
+  commit_id: bedc5b620384163abe272e913705fa23cfd3b3a3
   name: anomaly-detection
   ref: main
   repository: https://github.com/opensearch-project/anomaly-detection.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-asynchronous-search-1.1.0.0.zip
@@ -2445,18 +2463,21 @@ components:
   name: asynchronous-search
   ref: main
   repository: https://github.com/opensearch-project/asynchronous-search.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-reports-scheduler-1.1.0.0.zip
-  commit_id: edc55c09fc337117c04adeef51f5d2ddc54bd9df
+  commit_id: 622f334b0724e47f7ffd21cf7e7d521a9f6c949e
   name: dashboards-reports
   ref: main
   repository: https://github.com/opensearch-project/dashboards-reports.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-notebooks-1.1.0.0.zip
-  commit_id: 0016edddcb670b9ae93693623d37af5944921794
+  commit_id: 5a996eb8ec8c92e7469cd5e5f87b237352f60f61
   name: dashboards-notebooks
   ref: main
   repository: https://github.com/opensearch-project/dashboards-notebooks.git
+  version: 1.1.0.0
 schema-version: '1.0'
diff --git a/bundle-workflow/tests/tests_assemble_workflow/test_bundle_recorder.py b/bundle-workflow/tests/tests_assemble_workflow/test_bundle_recorder.py
--- a/bundle-workflow/tests/tests_assemble_workflow/test_bundle_recorder.py
+++ b/bundle-workflow/tests/tests_assemble_workflow/test_bundle_recorder.py
@@ -41,7 +41,7 @@ def test_record_component(self):
             {
                 "build": {
                     "architecture": "x64",
-                    "id": "f39b2d68f8b74389a0037f3e8d6ba0b0",
+                    "id": "c3ff7a232d25403fa8cc14c97799c323",
                     "location": "output_dir/opensearch-1.1.0-linux-x64.tar.gz",
                     "name": "OpenSearch",
                     "version": "1.1.0",
@@ -67,7 +67,7 @@ def test_get_manifest(self):
             {
                 "build": {
                     "architecture": "x64",
-                    "id": "f39b2d68f8b74389a0037f3e8d6ba0b0",
+                    "id": "c3ff7a232d25403fa8cc14c97799c323",
                     "location": "output_dir/opensearch-1.1.0-linux-x64.tar.gz",
                     "name": "OpenSearch",
                     "version": "1.1.0",
diff --git a/bundle-workflow/tests/tests_build_workflow/test_build_recorder.py b/bundle-workflow/tests/tests_build_workflow/test_build_recorder.py
--- a/bundle-workflow/tests/tests_build_workflow/test_build_recorder.py
+++ b/bundle-workflow/tests/tests_build_workflow/test_build_recorder.py
@@ -19,7 +19,7 @@ class TestBuildRecorder(unittest.TestCase):
     def setUp(self):
         self.maxDiff = None
         self.build_recorder = BuildRecorder(
-            "1", "output_dir", "OpenSearch", "1.1", "x64", False
+            "1", "output_dir", "OpenSearch", "1.1.0", "x64", False
         )
 
     @patch("shutil.copyfile")
@@ -35,11 +35,11 @@ def test_record_component_and_artifact(self, mock_makedirs, mock_copyfile):
         )
 
         self.build_recorder.record_artifact(
-            "common-utils", "/files", "../file1.jar", __file__
+            "common-utils", "files", "../file1.jar", __file__
         )
 
         self.build_recorder.record_artifact(
-            "common-utils", "/files", "../file2.jar", __file__
+            "common-utils", "files", "../file2.jar", __file__
         )
 
         self.assertEqual(
@@ -49,15 +49,16 @@ def test_record_component_and_artifact(self, mock_makedirs, mock_copyfile):
                     "architecture": "x64",
                     "id": "1",
                     "name": "OpenSearch",
-                    "version": "1.1",
+                    "version": "1.1.0",
                 },
                 "components": [
                     {
-                        "artifacts": {"/files": ["../file1.jar", "../file2.jar"]},
+                        "artifacts": {"files": ["../file1.jar", "../file2.jar"]},
                         "commit_id": "3913d7097934cbfe1fdcf919347f22a597d00b76",
                         "name": "common-utils",
                         "ref": "main",
                         "repository": "https://github.com/opensearch-project/common-utils",
+                        "version": "1.1.0.0",
                     }
                 ],
                 "schema-version": "1.0",
@@ -80,12 +81,140 @@ def test_record_artifact(self, mock_makedirs, mock_copyfile):
         )
 
         self.build_recorder.record_artifact(
-            "common-utils", "/files", "../file1.jar", __file__
+            "common-utils", "files", "../file1.jar", __file__
         )
 
         mock_makedirs.assert_called_with("output_dir/..", exist_ok=True)
         mock_copyfile.assert_called_with(__file__, "output_dir/../file1.jar")
 
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_zip_extension(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+            self.build_recorder.record_artifact(
+                "security", "plugins", "../file1.zip", "invalid.file"
+            )
+        self.assertEqual(
+            "Artifact invalid.file is invalid: not a zip file.",
+            context.exception.__str__(),
+        )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_zip_version(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+            self.build_recorder.record_artifact(
+                "security", "plugins", "../file1.zip", "invalid.zip"
+            )
+        self.assertEqual(
+            "Artifact invalid.zip is invalid: expected filename to include 1.1.0.0.",
+            context.exception.__str__(),
+        )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_version_properties_missing(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                ""
+            )
+            with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+                self.build_recorder.record_artifact(
+                    "security", "plugins", "../file1.zip", "valid-1.1.0.0.zip"
+                )
+            self.assertEqual(
+                "Artifact valid-1.1.0.0.zip is invalid: expected to have version=1.1.0.0, but none was found.",
+                context.exception.__str__(),
+            )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_version_properties_mismatch(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                "version=1.2.3.4"
+            )
+            with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+                self.build_recorder.record_artifact(
+                    "security", "plugins", "../file1.zip", "valid-1.1.0.0.zip"
+                )
+            self.assertEqual(
+                "Artifact valid-1.1.0.0.zip is invalid: expected to have version=1.1.0.0, but was 1.2.3.4.",
+                context.exception.__str__(),
+            )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_version_properties(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                "opensearch.version=1.1.0\nversion=1.1.0.0"
+            )
+            self.build_recorder.record_artifact(
+                "security", "plugins", "../file1.zip", "valid-1.1.0.0.zip"
+            )
+            manifest_dict = self.build_recorder.get_manifest().to_dict()
+            self.assertEqual(manifest_dict["build"]["version"], "1.1.0")
+            self.assertEqual(manifest_dict["components"][0]["version"], "1.1.0.0")
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_maven_version_properties_mismatch(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                "Implementation-Version: 1.2.3.4"
+            )
+            with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+                self.build_recorder.record_artifact(
+                    "security", "maven", "../file1.zip", "valid.jar"
+                )
+            self.assertEqual(
+                "Artifact valid.jar is invalid: expected to have Implementation-Version=any of ['1.1.0.0', '1.1.0', None], but was 1.2.3.4.",
+                context.exception.__str__(),
+            )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_maven_version_properties_none(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                ""
+            )
+            self.build_recorder.record_artifact(
+                "security", "maven", "../file1.jar", "valid.jar"
+            )
+            manifest_dict = self.build_recorder.get_manifest().to_dict()
+            self.assertEqual(manifest_dict["build"]["version"], "1.1.0")
+            self.assertEqual(
+                manifest_dict["components"][0]["artifacts"]["maven"], ["../file1.jar"]
+            )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_maven_artifact_after_checking_maven_version_properties(
+        self, *mocks
+    ):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                "Implementation-Version: 1.1.0.0"
+            )
+            self.build_recorder.record_artifact(
+                "security", "maven", "../file1.jar", "valid.jar"
+            )
+            manifest_dict = self.build_recorder.get_manifest().to_dict()
+            self.assertEqual(manifest_dict["build"]["version"], "1.1.0")
+            self.assertEqual(
+                manifest_dict["components"][0]["artifacts"]["maven"], ["../file1.jar"]
+            )
+
     def test_get_manifest(self):
         manifest = self.build_recorder.get_manifest()
         self.assertIs(type(manifest), BuildManifest)
@@ -96,7 +225,7 @@ def test_get_manifest(self):
                     "architecture": "x64",
                     "id": "1",
                     "name": "OpenSearch",
-                    "version": "1.1",
+                    "version": "1.1.0",
                 },
                 "components": [],
                 "schema-version": "1.0",
diff --git a/bundle-workflow/tests/tests_build_workflow/test_build_recorder_snapshot.py b/bundle-workflow/tests/tests_build_workflow/test_build_recorder_snapshot.py
new file mode 100644
--- /dev/null
+++ b/bundle-workflow/tests/tests_build_workflow/test_build_recorder_snapshot.py
@@ -0,0 +1,117 @@
+# 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 unittest
+from unittest.mock import MagicMock, patch
+
+from build_workflow.build_recorder import BuildRecorder
+
+
+class TestBuildRecorderSnapshot(unittest.TestCase):
+    def setUp(self):
+        self.maxDiff = None
+        self.build_recorder = BuildRecorder(
+            "1", "output_dir", "OpenSearch", "1.1.0", "x64", True
+        )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_component_and_artifact(self, mock_makedirs, mock_copyfile):
+        self.build_recorder.record_component(
+            "common-utils",
+            MagicMock(
+                url="https://github.com/opensearch-project/common-utils",
+                ref="main",
+                sha="3913d7097934cbfe1fdcf919347f22a597d00b76",
+            ),
+        )
+
+        self.assertEqual(
+            self.build_recorder.get_manifest().to_dict(),
+            {
+                "build": {
+                    "architecture": "x64",
+                    "id": "1",
+                    "name": "OpenSearch",
+                    "version": "1.1.0-SNAPSHOT",
+                },
+                "components": [
+                    {
+                        "artifacts": {},
+                        "commit_id": "3913d7097934cbfe1fdcf919347f22a597d00b76",
+                        "name": "common-utils",
+                        "ref": "main",
+                        "repository": "https://github.com/opensearch-project/common-utils",
+                        "version": "1.1.0.0-SNAPSHOT",
+                    }
+                ],
+                "schema-version": "1.0",
+            },
+        )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_zip_version(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+            self.build_recorder.record_artifact(
+                "security", "plugins", "../file1.zip", "invalid.zip"
+            )
+        self.assertEqual(
+            "Artifact invalid.zip is invalid: expected filename to include 1.1.0.0-SNAPSHOT.",
+            context.exception.__str__(),
+        )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_version_properties_missing(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                ""
+            )
+            with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+                self.build_recorder.record_artifact(
+                    "security", "plugins", "../file1.zip", "valid-1.1.0.0-SNAPSHOT.zip"
+                )
+            self.assertEqual(
+                "Artifact valid-1.1.0.0-SNAPSHOT.zip is invalid: expected to have version=1.1.0.0-SNAPSHOT, but none was found.",
+                context.exception.__str__(),
+            )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_version_properties_mismatch(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                "version=1.2.3.4"
+            )
+            with self.assertRaises(BuildRecorder.ArtifactInvalidError) as context:
+                self.build_recorder.record_artifact(
+                    "security", "plugins", "../file1.zip", "valid-1.1.0.0-SNAPSHOT.zip"
+                )
+            self.assertEqual(
+                "Artifact valid-1.1.0.0-SNAPSHOT.zip is invalid: expected to have version=1.1.0.0-SNAPSHOT, but was 1.2.3.4.",
+                context.exception.__str__(),
+            )
+
+    @patch("shutil.copyfile")
+    @patch("os.makedirs")
+    def test_record_artifact_check_plugin_version_properties(self, *mocks):
+        self.build_recorder.record_component("security", MagicMock())
+        with patch("build_workflow.build_recorder.ZipFile") as mock_zipfile:
+            mock_zipfile.return_value.__enter__.return_value.read.return_value.decode.return_value = (
+                "opensearch.version=1.1.0\nversion=1.1.0.0-SNAPSHOT"
+            )
+            self.build_recorder.record_artifact(
+                "security", "plugins", "../file1.zip", "valid-1.1.0.0-SNAPSHOT.zip"
+            )
+            manifest_dict = self.build_recorder.get_manifest().to_dict()
+            self.assertEqual(manifest_dict["build"]["version"], "1.1.0-SNAPSHOT")
+            self.assertEqual(
+                manifest_dict["components"][0]["version"], "1.1.0.0-SNAPSHOT"
+            )
diff --git a/bundle-workflow/tests/tests_manifests/data/opensearch-build-1.1.0.yml b/bundle-workflow/tests/tests_manifests/data/opensearch-build-1.1.0.yml
--- a/bundle-workflow/tests/tests_manifests/data/opensearch-build-1.1.0.yml
+++ b/bundle-workflow/tests/tests_manifests/data/opensearch-build-1.1.0.yml
@@ -1,6 +1,6 @@
 build:
   architecture: x64
-  id: f39b2d68f8b74389a0037f3e8d6ba0b0
+  id: c3ff7a232d25403fa8cc14c97799c323
   name: OpenSearch
   version: 1.1.0
 components:
@@ -2329,10 +2329,11 @@ components:
     - maven/org/opensearch/script-expert-scoring/1.1.0/script-expert-scoring-1.1.0.jar.sha256
     - maven/org/opensearch/script-expert-scoring/1.1.0/script-expert-scoring-1.1.0.jar
     - maven/org/opensearch/script-expert-scoring/1.1.0/script-expert-scoring-1.1.0.module.sha256
-  commit_id: 07a57d9bbb3922079b7bb1be83a01252f57f81ec
+  commit_id: b7334f49d530ffd1a3f7bd0e5832b9b2a9caa583
   name: OpenSearch
   ref: 1.x
   repository: https://github.com/opensearch-project/OpenSearch.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/common-utils/maven-metadata-local.xml
@@ -2344,6 +2345,7 @@ components:
   name: common-utils
   ref: main
   repository: https://github.com/opensearch-project/common-utils.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/opensearch-job-scheduler/maven-metadata-local.xml
@@ -2363,34 +2365,45 @@ components:
   name: job-scheduler
   ref: main
   repository: https://github.com/opensearch-project/job-scheduler.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-sql-1.1.0.0.zip
-  commit_id: 9ada74383a812baf534af8984a276303e4619574
+  commit_id: d68547d585092af1e053d01e1b834259723cd304
   name: sql
   ref: main
   repository: https://github.com/opensearch-project/sql.git
+  version: 1.1.0.0
 - artifacts:
+    maven:
+    - maven/org/opensearch/notification/maven-metadata-local.xml
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0-sources.jar
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0.pom
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0.jar
+    - maven/org/opensearch/notification/1.1.0.0/notification-1.1.0.0-javadoc.jar
     plugins:
     - plugins/opensearch-alerting-1.1.0.0.zip
-  commit_id: ecd283f5c7eac51902a43ef577e44fdccdc0e5ae
+  commit_id: 8024b8b9195f837e49e5bebd7f4a31dfc333eb4d
   name: alerting
   ref: main
   repository: https://github.com/opensearch-project/alerting.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-notifications-1.1.0.0.zip
-  commit_id: 7efc43a0a2f6829b170909ede56d70434d32ab1d
+  commit_id: d0d3e485c4a850f73652a989eeec795b7347fbb6
   name: notifications
   ref: main
   repository: https://github.com/opensearch-project/notifications.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-security-1.1.0.0.zip
-  commit_id: 25383effb9b7eeb3718e1fd7288e388a074568bf
+  commit_id: 534fffe0e6cf2b33b9abcbc6508e98fc2d077a3d
   name: security
-  ref: fix-snapshot-build
-  repository: https://github.com/dblock/security.git
+  ref: main
+  repository: https://github.com/opensearch-project/security.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/performanceanalyzer-rca/maven-metadata-local.xml
@@ -2401,6 +2414,7 @@ components:
   name: performance-analyzer-rca
   ref: main
   repository: https://github.com/opensearch-project/performance-analyzer-rca.git
+  version: 1.1.0.0
 - artifacts:
     maven:
     - maven/org/opensearch/opensearch-performance-analyzer/maven-metadata-local.xml
@@ -2411,17 +2425,19 @@ components:
     - maven/org/opensearch/opensearch-performance-analyzer/1.1.0.0/opensearch-performance-analyzer-1.1.0.0-sources.jar
     plugins:
     - plugins/opensearch-performance-analyzer-1.1.0.0.zip
-  commit_id: 95c2b6c7054f3e52df5d31d7859515b19bc95b59
+  commit_id: f184f0bc39302ac38af2585c663d619048f6cffe
   name: performance-analyzer
   ref: main
   repository: https://github.com/opensearch-project/performance-analyzer.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-index-management-1.1.0.0.zip
-  commit_id: ec0578934bae1fdee31ead07500ae8cee633c6d7
+  commit_id: 7897e9ae9cd5b49535e6a8bbf4c2f73cb458af24
   name: index-management
   ref: main
   repository: https://github.com/opensearch-project/index-management.git
+  version: 1.1.0.0
 - artifacts:
     libs:
     - libs/libKNNIndexV2_0_11.so
@@ -2431,13 +2447,15 @@ components:
   name: k-NN
   ref: main
   repository: https://github.com/opensearch-project/k-NN.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-anomaly-detection-1.1.0.0.zip
-  commit_id: b5d02ebfffab35a3918c3b603da328584497b589
+  commit_id: bedc5b620384163abe272e913705fa23cfd3b3a3
   name: anomaly-detection
   ref: main
   repository: https://github.com/opensearch-project/anomaly-detection.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-asynchronous-search-1.1.0.0.zip
@@ -2445,18 +2463,21 @@ components:
   name: asynchronous-search
   ref: main
   repository: https://github.com/opensearch-project/asynchronous-search.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-reports-scheduler-1.1.0.0.zip
-  commit_id: edc55c09fc337117c04adeef51f5d2ddc54bd9df
+  commit_id: 622f334b0724e47f7ffd21cf7e7d521a9f6c949e
   name: dashboards-reports
   ref: main
   repository: https://github.com/opensearch-project/dashboards-reports.git
+  version: 1.1.0.0
 - artifacts:
     plugins:
     - plugins/opensearch-notebooks-1.1.0.0.zip
-  commit_id: 0016edddcb670b9ae93693623d37af5944921794
+  commit_id: 5a996eb8ec8c92e7469cd5e5f87b237352f60f61
   name: dashboards-notebooks
   ref: main
   repository: https://github.com/opensearch-project/dashboards-notebooks.git
+  version: 1.1.0.0
 schema-version: '1.0'
diff --git a/bundle-workflow/tests/tests_manifests/data/opensearch-bundle-1.1.0.yml b/bundle-workflow/tests/tests_manifests/data/opensearch-bundle-1.1.0.yml
--- a/bundle-workflow/tests/tests_manifests/data/opensearch-bundle-1.1.0.yml
+++ b/bundle-workflow/tests/tests_manifests/data/opensearch-bundle-1.1.0.yml
@@ -1,11 +1,11 @@
 build:
   architecture: x64
-  id: f39b2d68f8b74389a0037f3e8d6ba0b0
+  id: c3ff7a232d25403fa8cc14c97799c323
   location: bundle/opensearch-1.1.0-linux-x64.tar.gz
   name: OpenSearch
   version: 1.1.0
 components:
-- commit_id: 07a57d9bbb3922079b7bb1be83a01252f57f81ec
+- commit_id: b7334f49d530ffd1a3f7bd0e5832b9b2a9caa583
   location: artifacts/bundle/opensearch-min-1.1.0-linux-x64.tar.gz
   name: OpenSearch
   ref: 1.x
diff --git a/bundle-workflow/tests/tests_manifests/test_build_manifest.py b/bundle-workflow/tests/tests_manifests/test_build_manifest.py
--- a/bundle-workflow/tests/tests_manifests/test_build_manifest.py
+++ b/bundle-workflow/tests/tests_manifests/test_build_manifest.py
@@ -36,7 +36,7 @@ def test_component(self):
             "https://github.com/opensearch-project/OpenSearch.git",
         )
         self.assertEqual(
-            opensearch_component.commit_id, "07a57d9bbb3922079b7bb1be83a01252f57f81ec"
+            opensearch_component.commit_id, "b7334f49d530ffd1a3f7bd0e5832b9b2a9caa583"
         )
         self.assertEqual(opensearch_component.ref, "1.x")
         self.assertEqual(
diff --git a/bundle-workflow/tests/tests_manifests/test_bundle_manifest.py b/bundle-workflow/tests/tests_manifests/test_bundle_manifest.py
--- a/bundle-workflow/tests/tests_manifests/test_bundle_manifest.py
+++ b/bundle-workflow/tests/tests_manifests/test_bundle_manifest.py
@@ -45,7 +45,7 @@ def test_component(self):
         )
         self.assertEqual(
             opensearch_min_component.commit_id,
-            "07a57d9bbb3922079b7bb1be83a01252f57f81ec",
+            "b7334f49d530ffd1a3f7bd0e5832b9b2a9caa583",
         )
         self.assertEqual(opensearch_min_component.ref, "1.x")
 
diff --git a/bundle-workflow/tests/tests_manifests/test_manifest.py b/bundle-workflow/tests/tests_manifests/test_manifest.py
--- a/bundle-workflow/tests/tests_manifests/test_manifest.py
+++ b/bundle-workflow/tests/tests_manifests/test_manifest.py
@@ -17,10 +17,10 @@ def setUp(self):
     def test_manifest_is_abstract(self):
         with self.assertRaises(TypeError) as context:
             Manifest(None)
-            self.assertEqual(
-                "Can't instantiate abstract class Manifest with abstract methods __init__",
-                context.exception.__str__(),
-            )
+        self.assertEqual(
+            "Can't instantiate abstract class Manifest with abstract methods __init__",
+            context.exception.__str__(),
+        )
 
     def test_invalid_version(self):
         class TestManifest(Manifest):
@@ -31,6 +31,6 @@ def __init__(self, data):
 
         with self.assertRaises(ValueError) as context:
             TestManifest.from_path(manifest_path)
-            self.assertEqual(
-                "Unsupported schema version: invalid", context.exception.__str__()
-            )
+        self.assertEqual(
+            "Unsupported schema version: invalid", context.exception.__str__()
+        )