This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new a5603943f8ad CAMEL-24252: Fix the container upgrade workflow aborting
the PR loop (#25088)
a5603943f8ad is described below
commit a5603943f8ad66b2bd2eb8b2dccf20de44147bd8
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Jul 24 14:05:07 2026 +0200
CAMEL-24252: Fix the container upgrade workflow aborting the PR loop
(#25088)
update-metadata-version.py exited 1 whenever no metadata entry matched,
and the workflow step runs under bash -e, so the first container whose
version has no serviceVersion counterpart aborted the whole loop: the
remaining containers never got a PR and the job went red.
Match the entries the way CamelTestInfraGenerateMetadataMojo derives
serviceVersion — the property key prefix against the entry aliases —
instead of the module directory name, so a container.properties shared
across modules (azure.container, owned by camel-test-infra-azure-common
but feeding the azure-storage-blob and azure-storage-queue entries) is
resolved. Platform-specific keys, version filters and properties with no
metadata counterpart are now reported as a no-op instead of an error,
and the workflow keeps going if the update fails anyway.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
.../test_update_metadata_version.py | 207 +++++++++++++++++++++
.../update-metadata-version.py | 110 ++++++++---
.github/workflows/check-container-versions.yml | 12 +-
.github/workflows/pr-ci-scripts-validation.yml | 47 +++++
.gitignore | 1 +
5 files changed, 350 insertions(+), 27 deletions(-)
diff --git
a/.github/actions/check-container-upgrade/test_update_metadata_version.py
b/.github/actions/check-container-upgrade/test_update_metadata_version.py
new file mode 100644
index 000000000000..4b0db7ecd18d
--- /dev/null
+++ b/.github/actions/check-container-upgrade/test_update_metadata_version.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Tests for update-metadata-version.py. Run with: python3 -m unittest
discover"""
+
+import contextlib
+import importlib.util
+import io
+import json
+import os
+import tempfile
+import unittest
+from pathlib import Path
+from unittest import mock
+
+HERE = Path(__file__).parent
+REPO_ROOT = HERE.parents[2]
+METADATA_INFRA = REPO_ROOT /
"test-infra/camel-test-infra-all/src/generated/resources/META-INF/metadata.json"
+
+_spec = importlib.util.spec_from_file_location("update_metadata_version", HERE
/ "update-metadata-version.py")
+updater = importlib.util.module_from_spec(_spec)
+_spec.loader.exec_module(updater)
+
+
+def entry(artifact_id, service_version, alias, alias_implementation=None):
+ return {
+ "alias": alias,
+ "aliasImplementation": alias_implementation or [],
+ "artifactId": artifact_id,
+ "serviceVersion": service_version,
+ }
+
+
+AZURE_BLOB = entry("camel-test-infra-azure-storage-blob", "3.35.0", ["azure"],
["storage-blob"])
+AZURE_QUEUE = entry("camel-test-infra-azure-storage-queue", "3.35.0",
["azure"], ["storage-queue"])
+OLLAMA = entry("camel-test-infra-ollama", "0.31.2", ["ollama"])
+HIVEMQ = entry("camel-test-infra-hivemq", "2025.5", ["hive-mq"])
+HIVEMQ_SPARKPLUG = entry("camel-test-infra-hivemq", "camel", ["hive-mq"],
["sparkplug"])
+OBSERVABILITY = entry("camel-test-infra-observability", None,
["observability"])
+
+
+class PropertyPrefixTest(unittest.TestCase):
+
+ def test_container_property_yields_normalized_prefix(self):
+ self.assertEqual("azure", updater.property_prefix("azure.container"))
+ self.assertEqual("hashicorpvault",
updater.property_prefix("hashicorp.vault.container"))
+ self.assertEqual("kafka",
updater.property_prefix("kafka.container.image"))
+
+ def test_platform_specific_property_has_no_target(self):
+ # The Mojo skips these keys, so their version never reaches
metadata.json
+ self.assertIsNone(updater.property_prefix("ollama.container.ppc64le"))
+ self.assertIsNone(updater.property_prefix("aws.container.s390x"))
+
self.assertIsNone(updater.property_prefix("tensorflow.serving.container.aarch64"))
+ self.assertIsNone(updater.property_prefix("artemis.container.amd64"))
+
+ def test_version_metadata_property_has_no_target(self):
+
self.assertIsNone(updater.property_prefix("ollama.container.version.exclude"))
+
self.assertIsNone(updater.property_prefix("mongodb.container.version.include"))
+
self.assertIsNone(updater.property_prefix("milvus.container.version.freeze.major"))
+
self.assertIsNone(updater.property_prefix("rocketmq.container.image.version"))
+
+ def test_non_container_property_has_no_target(self):
+ self.assertIsNone(updater.property_prefix("ollama.model"))
+ self.assertIsNone(updater.property_prefix("ollama.api.key"))
+
+
+class MatchesTest(unittest.TestCase):
+
+ def test_matches_service_alias(self):
+ self.assertTrue(updater.matches(OLLAMA, "ollama"))
+ self.assertFalse(updater.matches(OLLAMA, "milvus"))
+
+ def test_matches_dashed_alias(self):
+ self.assertTrue(updater.matches(HIVEMQ, "hivemq"))
+
+ def test_compound_prefix_matches_implementation_alias(self):
+ self.assertTrue(updater.matches(HIVEMQ_SPARKPLUG, "hivemqsparkplug"))
+ self.assertFalse(updater.matches(HIVEMQ, "hivemqsparkplug"))
+
+ def test_matches_entry_of_another_module(self):
+ # azure.container lives in camel-test-infra-azure-common
+ self.assertTrue(updater.matches(AZURE_BLOB, "azure"))
+ self.assertTrue(updater.matches(AZURE_QUEUE, "azure"))
+
+
+class UpdateMetadataTest(unittest.TestCase):
+
+ def setUp(self):
+ self.tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.tmp.cleanup)
+
+ def write(self, entries):
+ path = os.path.join(self.tmp.name, "metadata.json")
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(updater.jackson_format(entries))
+ return path
+
+ def read(self, path):
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+ def update(self, prefix, old_version, new_version, path):
+ with contextlib.redirect_stdout(io.StringIO()):
+ return updater.update_metadata(prefix, old_version, new_version,
path)
+
+ def run_main(self):
+ out = io.StringIO()
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(out):
+ updater.main()
+ return out.getvalue()
+
+ def test_updates_every_entry_fed_by_a_shared_property(self):
+ path = self.write([AZURE_BLOB, AZURE_QUEUE, OLLAMA])
+
+ self.assertTrue(self.update("azure", "3.35.0", "3.36.0", path))
+
+ versions = {e["artifactId"]: e["serviceVersion"] for e in
self.read(path)}
+ self.assertEqual("3.36.0",
versions["camel-test-infra-azure-storage-blob"])
+ self.assertEqual("3.36.0",
versions["camel-test-infra-azure-storage-queue"])
+ self.assertEqual("0.31.2", versions["camel-test-infra-ollama"])
+
+ def test_leaves_entries_on_another_version_alone(self):
+ path = self.write([AZURE_BLOB, AZURE_QUEUE])
+
+ self.assertFalse(self.update("azure", "3.30.0", "3.36.0", path))
+
+ self.assertEqual(["3.35.0", "3.35.0"], [e["serviceVersion"] for e in
self.read(path)])
+
+ def test_version_separates_entries_sharing_a_service_alias(self):
+ path = self.write([HIVEMQ, HIVEMQ_SPARKPLUG])
+
+ self.assertTrue(self.update("hivemq", "2025.5", "2025.6", path))
+
+ self.assertEqual(["2025.6", "camel"], [e["serviceVersion"] for e in
self.read(path)])
+
+ def test_missing_file_is_skipped(self):
+ self.assertFalse(
+ self.update("azure", "3.35.0", "3.36.0",
os.path.join(self.tmp.name, "absent.json")))
+
+ def test_main_updates_all_metadata_files(self):
+ first = self.write([AZURE_BLOB, AZURE_QUEUE])
+ second = os.path.join(self.tmp.name, "catalog.json")
+ with open(second, "w", encoding="utf-8") as f:
+ f.write(updater.jackson_format([AZURE_BLOB, AZURE_QUEUE]))
+
+ with mock.patch("sys.argv", ["prog", "azure.container", "3.35.0",
"3.36.0", first, second]):
+ self.run_main()
+
+ for path in (first, second):
+ self.assertEqual(["3.36.0", "3.36.0"], [e["serviceVersion"] for e
in self.read(path)])
+
+ def test_main_succeeds_when_the_property_has_no_metadata_target(self):
+ # CAMEL-24252: a bump with nothing to update must not abort the caller
+ path = self.write([OLLAMA])
+
+ with mock.patch("sys.argv", ["prog", "ollama.container.ppc64le",
"v0.17.6", "v0.24.0", path]):
+ output = self.run_main()
+
+ self.assertIn("nothing to update", output)
+ self.assertEqual("0.31.2", self.read(path)[0]["serviceVersion"])
+
+ def test_main_succeeds_when_no_entry_matches(self):
+ # CAMEL-24252: multi-container modules carry no serviceVersion
+ path = self.write([OBSERVABILITY])
+
+ with mock.patch("sys.argv", ["prog", "observability.perses.container",
"v0.53.1", "v0.54.0", path]):
+ output = self.run_main()
+
+ self.assertIn("nothing to update", output)
+ self.assertIsNone(self.read(path)[0]["serviceVersion"])
+
+ def test_main_rejects_missing_arguments(self):
+ with mock.patch("sys.argv", ["prog", "azure.container", "3.35.0"]):
+ with self.assertRaises(SystemExit) as raised:
+ self.run_main()
+
+ self.assertEqual(1, raised.exception.code)
+
+
+class JacksonFormatTest(unittest.TestCase):
+
+ @unittest.skipUnless(METADATA_INFRA.is_file(), f"{METADATA_INFRA} not
available")
+ def test_output_matches_the_generated_metadata(self):
+ # The formatter must reproduce the Maven build output byte for byte,
+ # otherwise every bump would rewrite the whole file
+ content = METADATA_INFRA.read_text(encoding="utf-8")
+
+ self.assertEqual(content, updater.jackson_format(json.loads(content)))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/actions/check-container-upgrade/update-metadata-version.py
b/.github/actions/check-container-upgrade/update-metadata-version.py
index b4357169df2f..cebcb629efa9 100644
--- a/.github/actions/check-container-upgrade/update-metadata-version.py
+++ b/.github/actions/check-container-upgrade/update-metadata-version.py
@@ -21,14 +21,35 @@ Update serviceVersion in test-infra metadata.json files
when a container
image version is bumped. Produces output matching Jackson's
DefaultPrettyPrinter
format so the result is identical to what the Maven build generates.
+Entries are matched the same way CamelTestInfraGenerateMetadataMojo derives
+serviceVersion: the property key prefix (the part before ".container") is
+matched against the entry aliasImplementation/alias values. Matching on the
+Maven module name does not work, because a container.properties file may be
+shared across modules — azure.container lives in camel-test-infra-azure-common
+while the metadata entries belong to camel-test-infra-azure-storage-blob and
+camel-test-infra-azure-storage-queue.
+
+Some bumps legitimately have no metadata counterpart, and are reported as a
+no-op rather than an error:
+ * platform-specific keys (.ppc64le/.s390x/.aarch64/.amd64), which the Mojo
skips
+ * multi-container modules whose entries have no serviceVersion
(observability)
+ * modules with no @InfraService entry at all (triton, tensorflow-serving,
...)
+
Usage:
- python3 update-metadata-version.py <artifact-id> <old-version>
<new-version> <metadata-file> [<metadata-file2> ...]
+ python3 update-metadata-version.py <property-name> <old-version>
<new-version> <metadata-file> [<metadata-file2> ...]
"""
import json
import os
import sys
+# Suffixes the metadata Mojo skips: platform-specific image variants never
+# reach metadata.json, so a bump of those keys has no serviceVersion to update.
+PLATFORM_SUFFIXES = (".ppc64le", ".s390x", ".aarch64", ".amd64")
+
+# Keys carrying version-check metadata rather than an image reference.
+METADATA_MARKERS = (".version.exclude", ".version.include", ".version.freeze")
+
def jackson_format(data):
"""Serialize JSON to match Jackson's DefaultPrettyPrinter output."""
@@ -52,8 +73,51 @@ def jackson_format(data):
return "\n".join(lines)
-def update_metadata(artifact_id, old_version, new_version, metadata_file):
- """Update serviceVersion for entries matching the artifact and old
version."""
+def normalize(value):
+ """Normalize an alias or property prefix for matching, as the Mojo does."""
+ return value.replace("-", "").replace(".", "").lower()
+
+
+def property_prefix(property_name):
+ """
+ Return the normalized prefix of a container property key, or None when the
+ key holds no image version the metadata could ever carry.
+ """
+ key = property_name.lower()
+
+ if key.endswith(PLATFORM_SUFFIXES) or key.endswith(".version"):
+ return None
+ if any(marker in key for marker in METADATA_MARKERS):
+ return None
+ if "container" not in key:
+ return None
+
+ index = key.find(".container")
+ if index <= 0:
+ return None
+
+ return normalize(key[:index])
+
+
+def matches(entry, prefix):
+ """
+ Check whether the metadata entry is the one the property key feeds: the
+ prefix must equal, or end with, one of the entry aliases — the suffix match
+ covers compound keys such as hivemq.sparkplug.container.
+ """
+ aliases = list(entry.get("aliasImplementation") or [])
+ aliases.extend(entry.get("alias") or [])
+
+ for alias in aliases:
+ normalized = normalize(alias)
+ if normalized and prefix.endswith(normalized):
+ return True
+
+ return False
+
+
+def update_metadata(prefix, old_version, new_version, metadata_file):
+ """Update serviceVersion for entries fed by the bumped property."""
if not os.path.isfile(metadata_file):
print(f"⚠️ {metadata_file} not found, skipping")
return False
@@ -63,10 +127,7 @@ def update_metadata(artifact_id, old_version, new_version,
metadata_file):
updated = False
for entry in data:
- if (
- entry.get("artifactId") == artifact_id
- and entry.get("serviceVersion") == old_version
- ):
+ if entry.get("serviceVersion") == old_version and matches(entry,
prefix):
entry["serviceVersion"] = new_version
updated = True
@@ -74,8 +135,6 @@ def update_metadata(artifact_id, old_version, new_version,
metadata_file):
with open(metadata_file, "w", encoding="utf-8") as f:
f.write(jackson_format(data))
print(f"✅ Updated serviceVersion {old_version} → {new_version} in
{metadata_file}")
- else:
- print(f"ℹ️ No matching entries for {artifact_id}/{old_version} in
{metadata_file}")
return updated
@@ -83,27 +142,32 @@ def update_metadata(artifact_id, old_version, new_version,
metadata_file):
def main():
if len(sys.argv) < 5:
print(
- "Usage: update-metadata-version.py <artifact-id> <old-version>
<new-version> <metadata-file> [<metadata-file2> ...]"
+ "Usage: update-metadata-version.py <property-name> <old-version>
<new-version> <metadata-file> [<metadata-file2> ...]"
)
sys.exit(1)
- artifact_id = sys.argv[1]
+ property_name = sys.argv[1]
old_version = sys.argv[2]
new_version = sys.argv[3]
metadata_files = sys.argv[4:]
- failed_metadata_files = []
- for metadata_file in metadata_files:
- if not update_metadata(artifact_id, old_version, new_version,
metadata_file):
- failed_metadata_files.append(metadata_file)
-
- if failed_metadata_files:
- print(
- "❌ No metadata target was updated in: "
- + ", ".join(failed_metadata_files),
- file=sys.stderr,
- )
- sys.exit(1)
+ prefix = property_prefix(property_name)
+ if prefix is None:
+ print(f"ℹ️ {property_name} has no serviceVersion in the metadata,
nothing to update")
+ return
+
+ untouched = [
+ metadata_file
+ for metadata_file in metadata_files
+ if not update_metadata(prefix, old_version, new_version, metadata_file)
+ ]
+
+ if len(untouched) == len(metadata_files):
+ print(f"ℹ️ No metadata entry matches {property_name}/{old_version},
nothing to update")
+ elif untouched:
+ # The metadata files mirror each other, so an update landing in only
+ # some of them means one of them is out of sync.
+ print("⚠️ No matching entry in: " + ", ".join(untouched),
file=sys.stderr)
if __name__ == "__main__":
diff --git a/.github/workflows/check-container-versions.yml
b/.github/workflows/check-container-versions.yml
index 9327a922bff8..48e904709ccb 100644
--- a/.github/workflows/check-container-versions.yml
+++ b/.github/workflows/check-container-versions.yml
@@ -306,12 +306,16 @@ jobs:
# Create and switch to new branch
git checkout -b "$BRANCH_NAME"
- # Update serviceVersion in metadata.json files
+ # Update serviceVersion in metadata.json files.
+ # A failure here must not abort the loop: the remaining containers
+ # still deserve their own PR.
METADATA_INFRA="test-infra/camel-test-infra-all/src/generated/resources/META-INF/metadata.json"
METADATA_CATALOG="catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/test-infra/metadata.json"
- python3
.github/actions/check-container-upgrade/update-metadata-version.py \
- "$MODULE_NAME" "$OLD_VERSION" "$NEW_VERSION" \
- "$METADATA_INFRA" "$METADATA_CATALOG"
+ if ! python3
.github/actions/check-container-upgrade/update-metadata-version.py \
+ "$PROPERTY_NAME" "$OLD_VERSION" "$NEW_VERSION" \
+ "$METADATA_INFRA" "$METADATA_CATALOG"; then
+ echo "⚠️ Failed to update the metadata for $PROPERTY_NAME,
continuing"
+ fi
# Add and commit changes
git add "$FILE_PATH" "$METADATA_INFRA" "$METADATA_CATALOG"
diff --git a/.github/workflows/pr-ci-scripts-validation.yml
b/.github/workflows/pr-ci-scripts-validation.yml
new file mode 100644
index 000000000000..9697d7c676f2
--- /dev/null
+++ b/.github/workflows/pr-ci-scripts-validation.yml
@@ -0,0 +1,47 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+name: PR CI scripts validation
+on:
+ pull_request:
+ branches:
+ - main
+ paths:
+ - '.github/actions/check-container-upgrade/**'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number ||
github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ if: github.repository == 'apache/camel'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout camel repo
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #
v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Test the container upgrade scripts
+ shell: bash
+ run: |
+ cd .github/actions/check-container-upgrade
+ python3 -m unittest discover --verbose
diff --git a/.gitignore b/.gitignore
index 9a9a12cf53b6..e03f8a64b89d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@ components/camel-cxf/activemq-data
.flattened-pom.xml
.java-version
node_modules/
+__pycache__/
mvnd.zip*
.camel-jbang
.camel-jbang-run