Skip to content

Commit f75a1b5

Browse files
authored
Merge pull request #601 from posit-dev/worktree-workbench-preview-channel
Add preview channel for workbench products; fix matrix+dev exclusion
2 parents 7061ee1 + 0907fe6 commit f75a1b5

7 files changed

Lines changed: 94 additions & 20 deletions

File tree

posit-bakery/posit_bakery/cli/ci.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,13 @@ def matrix(
140140
for img in images:
141141
entry = {"image": img.name}
142142
versions = img.versions
143-
if (img.matrix is None and matrix_versions == MatrixVersionInclusionEnum.ONLY) or (
144-
img.matrix is not None and matrix_versions == MatrixVersionInclusionEnum.EXCLUDE
145-
):
143+
if img.matrix is None and matrix_versions == MatrixVersionInclusionEnum.ONLY:
146144
continue
147-
elif img.matrix is not None and matrix_versions != MatrixVersionInclusionEnum.EXCLUDE:
148-
versions = img.matrix.to_image_versions()
145+
elif img.matrix is not None:
146+
if matrix_versions != MatrixVersionInclusionEnum.EXCLUDE:
147+
versions = img.matrix.to_image_versions()
148+
# If EXCLUDE: fall through using img.versions (devVersions are appended
149+
# there by load_dev_versions). The dev_versions filter below handles the rest.
149150
for ver in versions:
150151
included, _ = ver.matches_dev_filter(dev_versions, dev_channel)
151152
if not included:

posit-bakery/posit_bakery/config/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ def version_matches(ver_name: str, filter_version: str) -> bool:
266266
ver = ParsedVersion.parse(ver_name)
267267
filt = ParsedVersion.parse(filter_version)
268268
if ver is not None and filt is not None:
269+
if ver.dep_versions is not None or filt.dep_versions is not None:
270+
if ver.dep_versions is None or filt.dep_versions is None:
271+
return False
272+
return ver.dep_versions[: len(filt.dep_versions)] == filt.dep_versions
269273
return ver.release[: len(filt.release)] == filt.release and (
270274
filt.prerelease is None or ver.prerelease == filt.prerelease
271275
)

posit-bakery/posit_bakery/config/image/parsed_version.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,34 @@
1010
from dataclasses import dataclass
1111
from typing import TYPE_CHECKING
1212

13+
from posit_bakery.config.dependencies.const import SupportedDependencies
14+
1315
# Local under TYPE_CHECKING to avoid a circular import: version.py imports
1416
# this module at runtime, so importing ImageVersion eagerly would cycle.
1517
if TYPE_CHECKING:
1618
from posit_bakery.config.image.version import ImageVersion
1719

1820
log = logging.getLogger(__name__)
1921

20-
# Anchored grammar:
22+
# Alternation of known dependency name prefixes, longest first.
23+
_DEP_ALT = "|".join(re.escape(d.value) for d in sorted(SupportedDependencies, key=lambda d: len(d.value), reverse=True))
24+
25+
# Anchored grammar (calver/semver — no dep prefix):
2126
# <release> one or more dot-separated digit groups, minimum two groups
2227
# -<prerelease> optional, semver prerelease alphabet
2328
# +<build> optional, semver build alphabet
24-
_VERSION_RE = re.compile(
29+
_CALVER_RE = re.compile(
2530
r"^(?P<release>\d+(?:\.\d+)+)"
2631
r"(?:-(?P<prerelease>[0-9A-Za-z.-]+))?"
2732
r"(?:\+(?P<build>[0-9A-Za-z.-]+))?$"
2833
)
2934

35+
# Dep-prefixed version grammar (e.g. "R4.3.3" or "R4.3.3-python3.13.14"):
36+
# one or more {dep-name}{release} components separated by hyphens
37+
_SINGLE_DEP = r"(?:" + _DEP_ALT + r")\d+(?:\.\d+)+"
38+
_MATRIX_RE = re.compile(r"^" + _SINGLE_DEP + r"(?:-" + _SINGLE_DEP + r")*$")
39+
_DEP_PAIR_RE = re.compile(r"(?P<dep>" + _DEP_ALT + r")(?P<version>\d+(?:\.\d+)+)")
40+
3041

3142
@dataclass(frozen=True)
3243
class ParsedVersion:
@@ -43,6 +54,8 @@ class ParsedVersion:
4354
release: tuple[int, ...]
4455
prerelease: str | None = None
4556
build: str | None = None
57+
# Non-None for dep-prefixed strings; each entry is (dep_name, version_tuple).
58+
dep_versions: tuple[tuple[str, tuple[int, ...]], ...] | None = None
4659

4760
def __str__(self) -> str:
4861
return self.original
@@ -53,17 +66,23 @@ def parse(cls, value: str) -> "ParsedVersion | None":
5366
if not isinstance(value, str):
5467
log.warning("Unparseable version string: %r", value)
5568
return None
56-
match = _VERSION_RE.match(value)
57-
if match is None:
58-
log.warning("Unparseable version string: %r", value)
59-
return None
60-
release = tuple(int(part) for part in match.group("release").split("."))
61-
return cls(
62-
original=value,
63-
release=release,
64-
prerelease=match.group("prerelease"),
65-
build=match.group("build"),
66-
)
69+
match = _CALVER_RE.match(value)
70+
if match is not None:
71+
release = tuple(int(part) for part in match.group("release").split("."))
72+
return cls(
73+
original=value,
74+
release=release,
75+
prerelease=match.group("prerelease"),
76+
build=match.group("build"),
77+
)
78+
if _MATRIX_RE.match(value):
79+
pairs = tuple(
80+
(m.group("dep"), tuple(int(x) for x in m.group("version").split(".")))
81+
for m in _DEP_PAIR_RE.finditer(value)
82+
)
83+
return cls(original=value, release=pairs[0][1], dep_versions=pairs)
84+
log.warning("Unparseable version string: %r", value)
85+
return None
6786

6887
def _release_key(self, length: int) -> tuple[int, ...]:
6988
"""Zero-pad ``self.release`` to ``length`` for length-tolerant comparison."""

posit-bakery/posit_bakery/config/image/posit_product/const.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class ReleaseChannelEnum(str, Enum):
5252
PRODUCT_RELEASE_CHANNEL_SUPPORT_MAP = {
5353
ProductEnum.CONNECT: [ReleaseChannelEnum.RELEASE, ReleaseChannelEnum.DAILY],
5454
ProductEnum.PACKAGE_MANAGER: [ReleaseChannelEnum.RELEASE, ReleaseChannelEnum.PREVIEW, ReleaseChannelEnum.DAILY],
55-
ProductEnum.WORKBENCH: [ReleaseChannelEnum.RELEASE, ReleaseChannelEnum.DAILY],
55+
ProductEnum.WORKBENCH: [ReleaseChannelEnum.RELEASE, ReleaseChannelEnum.PREVIEW, ReleaseChannelEnum.DAILY],
56+
ProductEnum.WORKBENCH_SESSION: [ReleaseChannelEnum.RELEASE, ReleaseChannelEnum.PREVIEW, ReleaseChannelEnum.DAILY],
5657
}
5758

5859
PRODUCT_RELEASE_STREAM_SUPPORT_MAP = PRODUCT_RELEASE_CHANNEL_SUPPORT_MAP # deprecated alias, remove in Phase 0.5c

posit-bakery/posit_bakery/config/image/posit_product/main.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,17 @@ def get(self, metadata: dict, version_override: str | None = None) -> ReleaseCha
232232
),
233233
},
234234
),
235+
ReleaseChannelEnum.PREVIEW: ReleaseChannelPath(
236+
WORKBENCH_DAILY_URL,
237+
{
238+
"version": resolvers.StringMapPathResolver(
239+
["products", "workbench", "platforms", "{download_json_os}-{arch_identifier}", "version"]
240+
),
241+
"download_url": resolvers.StringMapPathResolver(
242+
["products", "workbench", "platforms", "{download_json_os}-{arch_identifier}", "link"]
243+
),
244+
},
245+
),
235246
ReleaseChannelEnum.DAILY: ReleaseChannelPath(
236247
WORKBENCH_DAILY_URL,
237248
{
@@ -256,6 +267,17 @@ def get(self, metadata: dict, version_override: str | None = None) -> ReleaseCha
256267
),
257268
},
258269
),
270+
ReleaseChannelEnum.PREVIEW: ReleaseChannelPath(
271+
WORKBENCH_DAILY_URL,
272+
{
273+
"version": resolvers.StringMapPathResolver(
274+
["products", "session", "platforms", "{download_json_os}-{arch_identifier}", "version"]
275+
),
276+
"download_url": resolvers.StringMapPathResolver(
277+
["products", "session", "platforms", "{download_json_os}-{arch_identifier}", "link"]
278+
),
279+
},
280+
),
259281
ReleaseChannelEnum.DAILY: ReleaseChannelPath(
260282
WORKBENCH_DAILY_URL,
261283
{

posit-bakery/test/cli/test_ci.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class TestVersionMatches:
144144
("2026.03.1", "2026.03"),
145145
("2026.05.0-dev+15-gSHA", "2026.05.0-dev"),
146146
("R4.5.3-python3.14.3", "R4.5.3-python3.14.3"),
147+
("R4.5.3-python3.14.3", "R4.5.3"),
147148
],
148149
)
149150
def test_matches(self, ver_name, filter_version):

posit-bakery/test/config/image/test_parsed_version.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class TestParseUnparseable:
1818
[
1919
"",
2020
"latest",
21-
"R4.3.3-python3.11.15",
2221
"v1.2.3",
2322
"not a version",
2423
"1", # only one release component
@@ -59,6 +58,10 @@ class TestParseRoundtrip:
5958
("2026.4.0-rc.1", (2026, 4, 0), "rc.1", None),
6059
# Edge: build only.
6160
("2026.4.0+abc", (2026, 4, 0), None, "abc"),
61+
# Dep-prefixed: single and compound matrix versions.
62+
("R4.3.3", (4, 3, 3), None, None),
63+
("python3.13.14", (3, 13, 14), None, None),
64+
("R4.3.3-python3.11.15", (4, 3, 3), None, None),
6265
],
6366
)
6467
def test_parses_and_roundtrips(self, value, release, prerelease, build, caplog):
@@ -185,3 +188,26 @@ def mock_iv(name: str, *, is_matrix: bool = False) -> MagicMock:
185188
# (Python's sort is stable). Parseable entries follow in ascending order.
186189
assert names[:2] == ["R4.3.3-python3.11.15", "garbage"]
187190
assert names[2:] == ["2026.04.0", "2026.04.1", "2026.05.0-dev+62-g1ca9367735"]
191+
192+
193+
class TestDepVersions:
194+
@pytest.mark.parametrize(
195+
"value,expected",
196+
[
197+
("R4.3.3", (("R", (4, 3, 3)),)),
198+
("python3.13.14", (("python", (3, 13, 14)),)),
199+
("R4.3.3-python3.11.15", (("R", (4, 3, 3)), ("python", (3, 11, 15)))),
200+
("R4.3-python3.11-quarto1.4", (("R", (4, 3)), ("python", (3, 11)), ("quarto", (1, 4)))),
201+
],
202+
)
203+
def test_dep_versions_populated(self, value, expected, caplog):
204+
caplog.set_level(logging.WARNING)
205+
parsed = ParsedVersion.parse(value)
206+
assert parsed is not None
207+
assert parsed.dep_versions == expected
208+
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
209+
210+
def test_pure_calver_has_no_dep_versions(self):
211+
parsed = ParsedVersion.parse("2026.04.0-dev+92")
212+
assert parsed is not None
213+
assert parsed.dep_versions is None

0 commit comments

Comments
 (0)