Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion pyartcd/pyartcd/pipelines/prepare_release_konflux.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@
import click
import semver
from artcommonlib import exectools
from artcommonlib.assembly import AssemblyTypes, assembly_config_struct, assembly_group_config
from artcommonlib.assembly import (
AssemblyTypes,
assembly_basis,
assembly_config_struct,
assembly_group_config,
assembly_own_issues_config,
)
from artcommonlib.constants import (
REGISTRY_QUAY_OCP_RELEASE_DEV,
SHIPMENT_DATA_URL_TEMPLATE,
Expand Down Expand Up @@ -376,6 +382,23 @@ async def check_blockers(self):
if self.assembly_type != AssemblyTypes.STANDARD:
self.logger.info(f"Skipping Blocker Bugs check for non-standard assembly {self.assembly}")
return

releases_model = Model(self.releases_config)
has_basis = bool(assembly_basis(releases_model, self.assembly).assembly)
if has_basis:
# Note: only reads issues.include (not include! or include?); operator-suffixed variants are not handled.
issues_config = assembly_own_issues_config(releases_model, self.assembly)
included_bug_ids = {str(i["id"]) for i in issues_config.include}
if included_bug_ids:
self.logger.info(
"Skipping blocker bug check: assembly %s is a targeted fix on basis assembly, "
"shipping %d bug(s): %s",
self.assembly,
len(included_bug_ids),
", ".join(sorted(included_bug_ids)),
)
return

self.logger.info(f"Checking Blocker Bugs for release {self.assembly}")
cmd = self._elliott_base_command + ["find-bugs:blocker", "--exclude-status=ON_QA"]
stdout = await self.execute_command_with_logging(cmd)
Expand Down
21 changes: 19 additions & 2 deletions pyartcd/pyartcd/pipelines/promote.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
go_arch_for_brew_arch,
go_suffix_for_arch,
)
from artcommonlib.assembly import AssemblyTypes, assembly_config_struct
from artcommonlib.assembly import AssemblyTypes, assembly_basis, assembly_config_struct, assembly_own_issues_config
from artcommonlib.constants import REGISTRY_CI_OPENSHIFT, REGISTRY_QUAY_OCP_RELEASE_DEV
from artcommonlib.exceptions import VerificationError
from artcommonlib.exectools import manifest_tool, manifest_tool_auth_file, to_thread
Expand Down Expand Up @@ -299,7 +299,24 @@ async def _run_pipeline(self):
else:
logger.info("Checking for blocker bugs...")
try:
await self.check_blocker_bugs()
releases_model = Model(releases_config)
has_basis = bool(assembly_basis(releases_model, self.assembly).assembly)
skip_blocker_check = False
if has_basis:
# Note: only reads issues.include (not include! or include?); operator-suffixed variants are not handled.
issues_config = assembly_own_issues_config(releases_model, self.assembly)
included_bug_ids = {str(i["id"]) for i in issues_config.include}
if included_bug_ids:
logger.info(
"Skipping blocker bug check: assembly %s is a targeted fix on basis assembly, "
"shipping %d bug(s): %s",
self.assembly,
len(included_bug_ids),
", ".join(sorted(included_bug_ids)),
)
skip_blocker_check = True
if not skip_blocker_check:
await self.check_blocker_bugs()
except VerificationError as err:
logger.warn("Blocker bugs found for release: %s", err)
justification = self._reraise_if_not_permitted(err, "BLOCKER_BUGS", permits)
Expand Down
87 changes: 87 additions & 0 deletions pyartcd/tests/pipelines/test_prepare_release_konflux.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,3 +981,90 @@ async def test_run_exits_unstable_when_deferred_build_failures_exist(
mock_set_shipment_mr_ready.assert_awaited_once()
mock_verify_payload.assert_awaited_once()
mock_report_deferred_build_failures.assert_awaited_once_with(pipeline.bundle_build_errors)

async def test_check_blockers_skips_when_basis_assembly_has_included_bugs(self):
"""Blocker check skipped when assembly has basis.assembly AND issues.include (targeted fix)."""
pipeline = PrepareReleaseKonfluxPipeline(
slack_client=self.mock_slack_client,
runtime=self.runtime,
group=self.group,
assembly=self.assembly,
)
pipeline.gitlab_token = self.gitlab_token
pipeline.assembly_type = AssemblyTypes.STANDARD
pipeline.releases_config = Model(
{
"releases": {
"test-assembly": {
"assembly": {
"type": AssemblyTypes.STANDARD.value,
"basis": {"assembly": "test-assembly-parent"},
"issues": {
"include": [
{"id": "OCPBUGS-85292"},
{"id": "OCPBUGS-99999"},
],
"exclude": [],
},
"group": {"product": "ocp", "release_date": "2025-Oct-22"},
}
}
}
}
)
pipeline.logger = Mock()
pipeline.execute_command_with_logging = AsyncMock()

await pipeline.check_blockers()

# Blocker check skipped — basis assembly + included bugs = targeted fix
pipeline.execute_command_with_logging.assert_not_called()

async def test_check_blockers_runs_when_no_basis_assembly(self):
"""Blocker check still runs when assembly has issues.include but no basis.assembly."""
pipeline = PrepareReleaseKonfluxPipeline(
slack_client=self.mock_slack_client,
runtime=self.runtime,
group=self.group,
assembly=self.assembly,
)
pipeline.gitlab_token = self.gitlab_token
pipeline.assembly_type = AssemblyTypes.STANDARD
pipeline.releases_config = Model(
{
"releases": {
"test-assembly": {
"assembly": {
"type": AssemblyTypes.STANDARD.value,
"issues": {
"include": [{"id": "OCPBUGS-85292"}],
},
}
}
}
}
)
pipeline.logger = Mock()
pipeline.execute_command_with_logging = AsyncMock(return_value="Found 0 bugs: ")

await pipeline.check_blockers()

# Blocker check runs because no basis.assembly
pipeline.execute_command_with_logging.assert_called_once()

async def test_check_blockers_skips_non_standard_assembly(self):
"""Test that check_blockers skips for non-standard assembly types."""
pipeline = PrepareReleaseKonfluxPipeline(
slack_client=self.mock_slack_client,
runtime=self.runtime,
group=self.group,
assembly=self.assembly,
)
pipeline.assembly_type = AssemblyTypes.STREAM
pipeline.logger = Mock()
pipeline.execute_command_with_logging = AsyncMock()

await pipeline.check_blockers()

# Verify that execute_command_with_logging was not called
pipeline.execute_command_with_logging.assert_not_called()
86 changes: 86 additions & 0 deletions pyartcd/tests/pipelines/test_promote.py
Original file line number Diff line number Diff line change
Expand Up @@ -2160,3 +2160,89 @@ async def test_push_manifest_list_uses_registry_config(
str(Path(temp_dir) / "4.10.99.manifest-list.yaml"),
],
)

@patch("pyartcd.pipelines.promote.RegistryConfig")
@patch("pyartcd.jira_client.JIRAClient.from_url", return_value=None)
@patch(
"pyartcd.pipelines.promote.util.load_releases_config",
return_value={
"releases": {
"4.10.99": {
"assembly": {
"type": "standard",
"basis": {"assembly": "4.10.98"},
"issues": {
"include": [{"id": "OCPBUGS-85292"}, {"id": "OCPBUGS-99999"}],
},
}
}
}
},
)
@patch(
"pyartcd.pipelines.promote.util.load_group_config",
return_value=Model(dict(arches=["x86_64", "s390x"], upgrades="4.10.98,4.9.99", advisories={"image": 2})),
)
async def test_run_skips_blocker_check_when_basis_assembly_has_included_bugs(
self, load_group_config: AsyncMock, load_releases_config: AsyncMock, _, __
):
"""Blocker bug check skipped when assembly has basis.assembly AND issues.include (targeted fix)."""
runtime = MagicMock(
config={
"build_config": {
"ocp_build_data_url": "https://example.com/ocp-build-data.git",
},
"jira": {
"url": JIRA_SERVER_URL,
},
},
working_dir=Path("/path/to/working"),
dry_run=False,
)
runtime.new_slack_client.return_value = AsyncMock()
runtime.new_slack_client.return_value.say.return_value = {'message': {'ts': ''}}
runtime.new_slack_client.return_value.bind_channel = MagicMock()

pipeline = await PromotePipeline.create(
runtime, group="openshift-4.10", assembly="4.10.99", signing_env="prod", skip_sigstore=True
)
pipeline.check_blocker_bugs = AsyncMock()
pipeline.change_advisory_state_qe = AsyncMock()
pipeline.get_advisory_info = AsyncMock(return_value={"id": 2, "errata_id": 2, "status": "QE"})

# Pipeline will fail later (no live ID), but blocker check should already have been skipped
with self.assertRaises(Exception):
await pipeline.run()

pipeline.check_blocker_bugs.assert_not_called()

@patch("pyartcd.pipelines.promote.exectools.cmd_gather_async")
async def test_check_blocker_bugs_no_excluded_bugs(self, cmd_gather_async: AsyncMock):
"""Test that check_blocker_bugs works without excluded bugs."""
runtime = MagicMock(
config={
"build_config": {
"ocp_build_data_url": "https://example.com/ocp-build-data.git",
},
"jira": {
"url": JIRA_SERVER_URL,
},
},
dry_run=False,
logger=MagicMock(),
new_slack_client=MagicMock(return_value=AsyncMock()),
)
with tempfile.TemporaryDirectory() as temp_dir:
runtime.working_dir = Path(temp_dir)
pipeline = PromotePipeline(runtime, group="openshift-4.10", assembly="4.10.99", signing_env="prod")

# Mock exectools.cmd_gather_async to return "Found 0 bugs"
cmd_gather_async.return_value = ("", "Found 0 bugs: ", "")

# Call check_blocker_bugs without excluded bug IDs
await pipeline.check_blocker_bugs()

# Verify the command does not include --exclude-bugs
call_args = cmd_gather_async.call_args[0][0]
self.assertIn("find-bugs:blocker", call_args)
self.assertNotIn("--exclude-bugs", call_args)
Loading