From c33b457e6858aa3ba41afdf79dda6b28bdc54f18 Mon Sep 17 00:00:00 2001 From: Chai Bot Date: Mon, 10 Aug 2026 22:01:37 +0000 Subject: [PATCH 1/2] Add streams.yml alias collision validation Detect collisions between top-level stream names and alias values in streams.yml. Two types of collisions are caught: - An alias value that matches a top-level stream name - The same alias value defined in multiple streams Follows the existing releases.py pattern for semantic validation that runs after schema validation. Co-Authored-By: Claude Opus 4.6 rh-pre-commit.version: 2.2.0 rh-pre-commit.check-secrets: ENABLED --- .../tests/test_streams.py | 100 ++++++++++++++++++ .../validator/__main__.py | 12 ++- ocp-build-data-validator/validator/streams.py | 46 ++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 ocp-build-data-validator/tests/test_streams.py create mode 100644 ocp-build-data-validator/validator/streams.py diff --git a/ocp-build-data-validator/tests/test_streams.py b/ocp-build-data-validator/tests/test_streams.py new file mode 100644 index 0000000000..0d4001d73f --- /dev/null +++ b/ocp-build-data-validator/tests/test_streams.py @@ -0,0 +1,100 @@ +import unittest + +from validator import streams + + +class TestStreams(unittest.TestCase): + def test_no_aliases_no_collision(self): + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + }, + "rhel": { + "image": "openshift/ose-base:ubi8", + }, + } + err = streams.validate(data) + self.assertIsNone(err) + + def test_aliases_no_collision(self): + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + "aliases": ["go-toolset"], + }, + "rhel": { + "image": "openshift/ose-base:ubi8", + "aliases": ["base-rhel"], + }, + } + err = streams.validate(data) + self.assertIsNone(err) + + def test_alias_collides_with_stream_name(self): + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + "aliases": ["rhel"], + }, + "rhel": { + "image": "openshift/ose-base:ubi8", + }, + } + err = streams.validate(data) + self.assertIsNotNone(err) + self.assertIn("Alias 'rhel' in stream 'golang' collides with a top-level stream name", err) + + def test_alias_collides_with_own_stream_name(self): + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + "aliases": ["golang"], + }, + } + err = streams.validate(data) + self.assertIsNotNone(err) + self.assertIn("Alias 'golang' in stream 'golang' collides with a top-level stream name", err) + + def test_duplicate_alias_across_streams(self): + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + "aliases": ["go-toolset"], + }, + "golang-alt": { + "image": "openshift/golang-builder:v2.0", + "aliases": ["go-toolset"], + }, + } + err = streams.validate(data) + self.assertIsNotNone(err) + self.assertIn("Alias 'go-toolset' is defined in both stream 'golang' and stream 'golang-alt'", err) + + def test_multiple_collisions_reported(self): + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + "aliases": ["rhel", "shared-alias"], + }, + "rhel": { + "image": "openshift/ose-base:ubi8", + "aliases": ["shared-alias"], + }, + } + err = streams.validate(data) + self.assertIsNotNone(err) + self.assertIn("Alias 'rhel' in stream 'golang' collides with a top-level stream name", err) + self.assertIn("Alias 'shared-alias' is defined in both stream 'golang' and stream 'rhel'", err) + + def test_empty_aliases_list_no_collision(self): + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + "aliases": [], + }, + "rhel": { + "image": "openshift/ose-base:ubi8", + }, + } + err = streams.validate(data) + self.assertIsNone(err) diff --git a/ocp-build-data-validator/validator/__main__.py b/ocp-build-data-validator/validator/__main__.py index f4eed2b389..f6969257a6 100644 --- a/ocp-build-data-validator/validator/__main__.py +++ b/ocp-build-data-validator/validator/__main__.py @@ -4,7 +4,7 @@ import sys from multiprocessing import Pool, cpu_count -from . import exceptions, format, github, global_session, releases, schema, support +from . import exceptions, format, github, global_session, releases, schema, streams, support def validate(file, schema_only, images_dir): @@ -25,7 +25,7 @@ def validate(file, schema_only, images_dir): msg = 'Schema mismatch: {}\nReturned error: {}'.format(file, err) support.fail_validation(msg, parsed) - if support.get_artifact_type(file) not in ['image', 'rpm', 'releases', 'group']: + if support.get_artifact_type(file) not in ['image', 'rpm', 'releases', 'group', 'streams']: print(f'✅ Validated {file}') return @@ -46,6 +46,14 @@ def validate(file, schema_only, images_dir): print(f'✅ Validated {file}') return + if support.get_artifact_type(file) == 'streams': + err = streams.validate(parsed) + if err: + msg = "streams.yml validation failed\nReturned error: {}".format(err) + support.fail_validation(msg, parsed) + print(f'✅ Validated {file}') + return + group_cfg = support.load_group_config_for(file) (url, err) = github.validate(parsed, group_cfg) diff --git a/ocp-build-data-validator/validator/streams.py b/ocp-build-data-validator/validator/streams.py new file mode 100644 index 0000000000..7345441a6c --- /dev/null +++ b/ocp-build-data-validator/validator/streams.py @@ -0,0 +1,46 @@ +from typing import Optional + + +def validate(data: dict) -> Optional[str]: + errors = [] + + alias_collision_error = get_alias_collisions(data) + if alias_collision_error: + errors.append(alias_collision_error) + + if errors: + return "; ".join(errors) + + +def get_alias_collisions(streams_data: dict) -> Optional[str]: + """ + Check for collisions between top-level stream names and alias values. + + A collision occurs when: + - An alias value matches a top-level stream name + - The same alias value appears in multiple streams + """ + stream_names = set(streams_data.keys()) + alias_to_stream = {} # Maps each alias value to the stream that defines it + collisions = [] + + for stream_name, stream_config in streams_data.items(): + aliases = stream_config.get("aliases", []) + if not aliases: + continue + + for alias in aliases: + # Check if alias collides with a top-level stream name + if alias in stream_names: + collisions.append(f"Alias '{alias}' in stream '{stream_name}' collides with a top-level stream name") + + # Check if alias is defined by another stream + if alias in alias_to_stream: + collisions.append( + f"Alias '{alias}' is defined in both stream '{alias_to_stream[alias]}' and stream '{stream_name}'" + ) + else: + alias_to_stream[alias] = stream_name + + if collisions: + return "Stream alias collisions found: " + "; ".join(collisions) From 1057a29d497baf6598cc6f495d1d85a2161e0f44 Mon Sep 17 00:00:00 2001 From: Chai Bot Date: Tue, 11 Aug 2026 03:00:28 +0000 Subject: [PATCH 2/2] Fix duplicate alias check to only flag cross-stream collisions The previous check incorrectly flagged duplicate aliases within the same stream as cross-stream collisions. Now only aliases shared between different streams are reported as errors. Co-Authored-By: Claude Opus 4.6 rh-pre-commit.version: 2.2.0 rh-pre-commit.check-secrets: ENABLED --- ocp-build-data-validator/tests/test_streams.py | 11 +++++++++++ ocp-build-data-validator/validator/streams.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ocp-build-data-validator/tests/test_streams.py b/ocp-build-data-validator/tests/test_streams.py index 0d4001d73f..819ce0e92b 100644 --- a/ocp-build-data-validator/tests/test_streams.py +++ b/ocp-build-data-validator/tests/test_streams.py @@ -86,6 +86,17 @@ def test_multiple_collisions_reported(self): self.assertIn("Alias 'rhel' in stream 'golang' collides with a top-level stream name", err) self.assertIn("Alias 'shared-alias' is defined in both stream 'golang' and stream 'rhel'", err) + def test_duplicate_alias_within_same_stream(self): + """Duplicate aliases within the same stream should not be flagged as cross-stream collisions.""" + data = { + "golang": { + "image": "openshift/golang-builder:v1.0", + "aliases": ["go-toolset", "go-toolset"], + }, + } + err = streams.validate(data) + self.assertIsNone(err) + def test_empty_aliases_list_no_collision(self): data = { "golang": { diff --git a/ocp-build-data-validator/validator/streams.py b/ocp-build-data-validator/validator/streams.py index 7345441a6c..550fb7ba4e 100644 --- a/ocp-build-data-validator/validator/streams.py +++ b/ocp-build-data-validator/validator/streams.py @@ -35,11 +35,11 @@ def get_alias_collisions(streams_data: dict) -> Optional[str]: collisions.append(f"Alias '{alias}' in stream '{stream_name}' collides with a top-level stream name") # Check if alias is defined by another stream - if alias in alias_to_stream: + if alias in alias_to_stream and alias_to_stream[alias] != stream_name: collisions.append( f"Alias '{alias}' is defined in both stream '{alias_to_stream[alias]}' and stream '{stream_name}'" ) - else: + elif alias not in alias_to_stream: alias_to_stream[alias] = stream_name if collisions: