Skip to content
Merged
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
32 changes: 22 additions & 10 deletions posit-bakery/posit_bakery/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,9 @@ def _apply_dev_spec(image: Image, settings: BakerySettings) -> None:
Called before load_dev_versions() so the pinned version is set
when resolution runs.
"""
# Local import to avoid circular import.
# Local imports to avoid circular import.
from posit_bakery.config.image.dev_version.channel import ImageDevelopmentVersionFromProductChannel
from posit_bakery.config.image.dev_version.dependency import ImageDevelopmentVersionFromDependency

# Validate channel consistency: if both are set and differ, the pinned version
# would be filtered out by --dev-channel. Fail loudly instead of silently skipping.
Expand All @@ -421,8 +422,8 @@ def _apply_dev_spec(image: Image, settings: BakerySettings) -> None:
candidates = [
dv
for dv in image.devVersions
if isinstance(dv, ImageDevelopmentVersionFromProductChannel)
and (target_channel is None or dv.channel == target_channel)
if isinstance(dv, (ImageDevelopmentVersionFromProductChannel, ImageDevelopmentVersionFromDependency))
and (target_channel is None or dv.get_release_channel() == target_channel)
]
if not candidates:
return
Expand All @@ -432,13 +433,24 @@ def _apply_dev_spec(image: Image, settings: BakerySettings) -> None:
f"channel '{target_channel}'. Specify 'channel' in --dev-spec or pass "
f"--dev-channel to disambiguate."
)
candidates[0].version_override = settings.dev_spec.version # None for branch-only specs
if settings.dev_spec.version is not None:
# Extract YYYY.MM from the pinned version to target the correct branch URL.
# The dailies API supports YYYY.MM path segments (e.g. /rstudio/2026.06/index.json).
candidates[0].release_branch = _extract_calver_minor(settings.dev_spec.version)
elif settings.dev_spec.release_branch is not None:
candidates[0].release_branch = settings.dev_spec.release_branch

candidate = candidates[0]
candidate.version_override = settings.dev_spec.version # None for branch-only specs

if isinstance(candidate, ImageDevelopmentVersionFromProductChannel):
if settings.dev_spec.version is not None:
# Extract YYYY.MM from the pinned version to target the correct branch URL.
# The dailies API supports YYYY.MM path segments (e.g. /rstudio/2026.06/index.json).
candidate.release_branch = _extract_calver_minor(settings.dev_spec.version)
elif settings.dev_spec.release_branch is not None:
candidate.release_branch = settings.dev_spec.release_branch
elif settings.dev_spec.version is None:
# Dependency-sourced dev versions resolve from a fixed dependency endpoint
# with no release-branch concept, so a branch-only spec cannot pin them.
log.warning(
f"Image '{image.name}': --dev-spec without a version cannot pin a "
f"dependency-sourced dev version; building the latest resolved version."
)


class BakeryConfig:
Expand Down
11 changes: 11 additions & 0 deletions posit-bakery/posit_bakery/config/image/dev_version/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ class BaseImageDevelopmentVersion(BakeryYAMLModel, abc.ABC):
description="Arbitrary key-value pairs used in template rendering.",
),
]
version_override: Annotated[
str | None,
Field(
exclude=True,
default=None,
description="Version pinned by a workflow dispatch spec (--dev-spec). When set, "
"bypasses CDN/dependency discovery so the build targets exactly this version. "
"The stream model also forwards it to the channel resolver for URL construction; "
"the dependency model returns it directly from get_version().",
),
]

@field_validator("extraRegistries", "overrideRegistries", mode="after")
@classmethod
Expand Down
10 changes: 0 additions & 10 deletions posit-bakery/posit_bakery/config/image/dev_version/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,6 @@ class ImageDevelopmentVersionFromProductChannel(BaseImageDevelopmentVersion):
"for builds targeting older versions.",
),
]
version_override: Annotated[
str | None,
Field(
exclude=True,
default=None,
description="Version pinned by a workflow dispatch spec. When set, bypasses CDN "
"discovery and is forwarded to the channel resolver for offline template rendering "
"(PPM) or manifest assertion (Connect, Workbench).",
),
]
release_branch: Annotated[
str | None,
Field(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def channel_not_release(cls, v: ReleaseChannelEnum | None) -> ReleaseChannelEnum
return v

def get_version(self) -> str:
if self.version_override is not None:
return self.version_override
constraint_class = get_dependency_constraint_class(self.dependency)
constraint = constraint_class(
prerelease=self.prerelease,
Expand Down
21 changes: 21 additions & 0 deletions posit-bakery/test/config/image/dev_version/test_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,24 @@ def test_repr_contains_key_fields(self):
assert 'sourceType="dependency"' in r
assert "positron" in r
assert "prerelease=True" in r


class TestVersionOverride:
def test_version_override_defaults_none(self):
"""The shared version_override field defaults to None."""
dev = ImageDevelopmentVersionFromDependency(
dependency="positron",
os=[_UBUNTU_24_OS],
)
assert dev.version_override is None

def test_version_override_short_circuits_resolution(self):
"""When version_override is set, get_version() returns it without a network call."""
dev = ImageDevelopmentVersionFromDependency(
dependency="positron",
prerelease=True,
os=[_UBUNTU_24_OS],
)
dev.version_override = "2026.06.0-99"
# No patch_requests_get fixture: a network call would error, proving none happens.
assert dev.get_version() == "2026.06.0-99"
80 changes: 80 additions & 0 deletions posit-bakery/test/config/test_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging
import os
import shutil
import textwrap
Expand Down Expand Up @@ -2329,3 +2330,82 @@ def test_version_takes_precedence_over_release_branch(self, tmp_path):
def test_extract_calver_minor_rejects_trailing_garbage(self):
with pytest.raises(ValueError, match="not a valid CalVer"):
_extract_calver_minor("2026.06.0-daily+143 trailing garbage")


class TestApplyDevSpecDependency:
"""--dev-spec pins dependency-sourced dev versions, matched by channel."""

def _make_image(self, tmp_path, *, channel="daily", extra_dev_versions=None):
"""Return a minimal Image with one positron daily dependency dev version."""
dev_version = {
"sourceType": "dependency",
"dependency": "positron",
"prerelease": True,
"channel": channel,
"os": [{"name": "Ubuntu 24.04", "primary": True}],
}
doc = BakeryConfigDocument(
base_path=tmp_path,
**{
"repository": {"url": "https://github.com/posit-dev/test"},
"images": [
{
"name": "test-positron-init",
"devVersions": [dev_version, *(extra_dev_versions or [])],
}
],
},
)
return doc.images[0]

def test_version_pins_dependency_dev_version(self, tmp_path):
"""version in DevBuildSpec sets version_override on the matching dependency dev version."""
image = self._make_image(tmp_path)
spec = DevBuildSpec(version="2026.06.0-99", channel="daily")
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
_apply_dev_spec(image, settings)
assert image.devVersions[0].version_override == "2026.06.0-99"

def test_channel_mismatch_is_noop(self, tmp_path):
"""A dev-spec channel that matches no dev version leaves version_override unset."""
image = self._make_image(tmp_path, channel="daily")
spec = DevBuildSpec(version="2026.06.0-99", channel="preview")
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
_apply_dev_spec(image, settings)
assert image.devVersions[0].version_override is None

def test_release_branch_ignored_for_dependency(self, tmp_path):
"""release_branch is not applicable to dependency dev versions and is not set."""
image = self._make_image(tmp_path)
spec = DevBuildSpec(version="2026.06.0-99", channel="daily", release_branch="apple-blossom")
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
_apply_dev_spec(image, settings)
dv = image.devVersions[0]
assert dv.version_override == "2026.06.0-99"
assert not hasattr(dv, "release_branch")

def test_branch_only_spec_skips_dependency_with_warning(self, tmp_path, caplog):
"""A branch-only spec cannot pin a dependency dev version: warns, leaves override None."""
image = self._make_image(tmp_path)
spec = DevBuildSpec(release_branch="apple-blossom")
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
with caplog.at_level(logging.WARNING):
_apply_dev_spec(image, settings)
assert image.devVersions[0].version_override is None
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
assert len(warnings) == 1
assert "cannot pin a dependency-sourced dev version" in warnings[0].message

def test_ambiguous_candidates_raise(self, tmp_path):
"""Two dev versions matching the same channel raise a disambiguation error."""
extra = {
"sourceType": "stream",
"product": "workbench",
"channel": "daily",
"os": [{"name": "Ubuntu 24.04", "primary": True}],
}
image = self._make_image(tmp_path, channel="daily", extra_dev_versions=[extra])
spec = DevBuildSpec(version="2026.06.0-99", channel="daily")
settings = BakerySettings(dev_versions=DevVersionInclusionEnum.ONLY, dev_spec=spec)
with pytest.raises(ValueError, match="dev versions matching"):
_apply_dev_spec(image, settings)
Loading