From 7b7c3bb16d3f830e193e342f9918e4f20605069e Mon Sep 17 00:00:00 2001 From: "Benjamin R. J. Schwedler" Date: Thu, 18 Jun 2026 09:08:11 -0500 Subject: [PATCH] Filter ci matrix by dev-spec channel A dispatched dev build folds its channel into the --dev-spec JSON, and the shared CI workflow then stops passing --dev-channel. bakery ci matrix filtered only by the explicit --dev-channel option, so a single-channel dispatch still emitted every channel's dev version. Only the matching channel got its version pinned. The others resolved to their channel head, producing surprise extra builds. Derive the matrix channel filter from the dev-spec channel when --dev-channel is absent. This matches the channel that _apply_dev_spec already uses to pin the version, so the filter and the pin agree. --- posit-bakery/posit_bakery/cli/ci.py | 10 ++- .../test/cli/test_ci_matrix_dev_versions.py | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/posit-bakery/posit_bakery/cli/ci.py b/posit-bakery/posit_bakery/cli/ci.py index e8183fa5..0c0c10f6 100644 --- a/posit-bakery/posit_bakery/cli/ci.py +++ b/posit-bakery/posit_bakery/cli/ci.py @@ -135,6 +135,14 @@ def matrix( c = BakeryConfig.from_context(context=context, settings=settings) images = [i for i in c.model.images if image_name is None or re.search(image_name, i.name) is not None] + # A --dev-spec carrying a channel implies the matrix should be filtered to that + # channel. The shared CI workflow folds the dispatched channel into the dev-spec + # and stops passing --dev-channel, so without this the other channels' dev versions + # would still be emitted (only the matching one gets its version pinned). + effective_dev_channel = settings.dev_channel + if effective_dev_channel is None and settings.dev_spec is not None: + effective_dev_channel = settings.dev_spec.channel + data = [] for img in images: entry = {"image": img.name} @@ -153,7 +161,7 @@ def matrix( # If EXCLUDE: fall through using img.versions (devVersions are appended # there by load_dev_versions). The dev_versions filter below handles the rest. for ver in versions: - included, _ = ver.matches_dev_filter(dev_versions, dev_channel) + included, _ = ver.matches_dev_filter(dev_versions, effective_dev_channel) if not included: continue if image_version is not None and not version_matches(ver.name, image_version): diff --git a/posit-bakery/test/cli/test_ci_matrix_dev_versions.py b/posit-bakery/test/cli/test_ci_matrix_dev_versions.py index cf9937c9..bb15e104 100644 --- a/posit-bakery/test/cli/test_ci_matrix_dev_versions.py +++ b/posit-bakery/test/cli/test_ci_matrix_dev_versions.py @@ -171,3 +171,72 @@ def test_prod_versions_excluded(self, mock_config_with_matrix_dev_image): versions_in_output = {e["version"] for e in data} assert prod_ver1.name not in versions_in_output assert prod_ver2.name not in versions_in_output + + +@pytest.fixture +def mock_config_with_two_channel_dev_image(): + """Patch BakeryConfig to return one non-matrix image carrying both a + daily and a preview dev version (the workbench/session-init shape).""" + daily = _make_version("2026.99.0+237", is_dev=True, channel=ReleaseChannelEnum.DAILY) + preview = _make_version("2026.99.0+240", is_dev=True, channel=ReleaseChannelEnum.PREVIEW) + img = MagicMock() + img.name = "workbench" + img.matrix = None + img.versions = [daily, preview] + + with patch("posit_bakery.cli.ci.BakeryConfig") as mock: + instance = MagicMock() + instance.model.images = [img] + mock.from_context.return_value = instance + yield mock, daily, preview + + +class TestCiMatrixDevSpecChannelFilter: + """A --dev-spec carrying a channel filters the matrix to that channel even + when --dev-channel is omitted. The shared workflow folds the channel into + the dev-spec and drops --dev-channel, so the matrix must honor it.""" + + def test_filters_to_dev_spec_channel(self, mock_config_with_two_channel_dev_image): + _, daily, preview = mock_config_with_two_channel_dev_image + result = runner.invoke( + app, + [ + "ci", + "matrix", + "--context", + BASIC_CONTEXT, + "--dev-versions", + "only", + "--dev-spec", + '{"version": "2026.99.0+240", "channel": "preview"}', + ], + catch_exceptions=False, + ) + assert result.exit_code == 0, result.output + data = json.loads(result.stdout.strip()) + versions = {e["version"] for e in data} + assert preview.name in versions + assert daily.name not in versions + + def test_branch_only_dev_spec_does_not_filter(self, mock_config_with_two_channel_dev_image): + """A branch-only dev-spec carries no channel, so all channels stay in the matrix.""" + _, daily, preview = mock_config_with_two_channel_dev_image + result = runner.invoke( + app, + [ + "ci", + "matrix", + "--context", + BASIC_CONTEXT, + "--dev-versions", + "only", + "--dev-spec", + '{"release_branch": "2026.06"}', + ], + catch_exceptions=False, + ) + assert result.exit_code == 0, result.output + data = json.loads(result.stdout.strip()) + versions = {e["version"] for e in data} + assert preview.name in versions + assert daily.name in versions