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