Skip to content

Image UID does not encode release stream, letting dev builds collide with release builds in bakery ci merge #553

@ianpittwood

Description

@ianpittwood

Summary

A release-tag push for Connect 2026.05.0 triggered the Development workflow alongside the Production workflow. Even though the dev build ran with bakery build --dev-versions only (which skips the release version), the ephemeral dev image definition was assigned a UID identical to the release version's UID. When bakery ci merge later read the build metadata files, it matched the dev-stream build metadata against the release targets and treated the development build as if it were the release build — pushing dev artifacts to the production registries (Docker Hub + GHCR) instead of GHCR-only with a -preview suffix.

This is the same class of bug previously hit with Package Manager, which is why its preview stream was disabled.

Reported by Byonca Honaker while shepherding the Connect May release (Slack thread).

Root cause

ImageTarget.uid() builds the identifier from image name, version, variant, and OS only — it does not include the release stream:

# posit-bakery/posit_bakery/image/image_target.py:285-294
def uid(self) -> str:
    """Generate a unique identifier for the target based on its properties."""
    u = f"{self.image_name}-{self.image_version.name}"
    if self.image_variant:
        u += f"-{self.image_variant.name}"
    if self.image_os:
        u += f"-{self.image_os.name}"
    return re.sub("[ .+/]", "-", u).lower()

The release stream is tracked, but only in image_version.metadata["release_stream"] (ReleaseStreamEnum = release / preview / daily, see posit_bakery/config/image/posit_product/const.py:46-49), not in the UID. Development versions get a distinct render subpath (.dev-{version}, posit_bakery/config/image/dev_version/base.py:258) but the same version name, so:

  • release connect-2026.05.0-<variant>-<os>
  • dev (daily/preview) 2026.05.0 → ephemeral .dev-2026.05.0

both produce UID connect-2026.05.0-<variant>-<os>.

bakery ci merge keys build metadata by UID and matches it back onto targets:

# posit_bakery/config/config.py:915-925
def _merge_sequential_build_metadata_files(self) -> dict[str, Any]:
    merged_metadata: dict[str, dict[str, Any]] = {}
    for target in self.targets:
        for build_metadata in target.build_metadata:
            merged_metadata[target.uid] = build_metadata.model_dump(exclude_none=True, by_alias=True)
    return merged_metadata
# posit_bakery/config/config.py:927-942 (load side)
for target in self.targets:
    result = target.load_build_metadata_from_file(metadata_file)  # matches by UID
    if result is not None:
        targets_loaded.append(target.uid)

Because the UID has no stream component, dev metadata silently satisfies the release target lookup.

Impact

  • Development-stream builds can be merged into and pushed as release artifacts, overwriting production tags in GHCR (and reaching Docker Hub).
  • Requires a manual re-run of the Production build to rewrite the registries after the fact.
  • Triggered by the normal just tag-release flow (push of a release tag), so any product release is exposed to this edge case.

Evidence (Connect May release)

Proposed fix

Differentiate the streams at the UID level and/or fail fast on a collision:

  1. Include the release stream as a component of the UID (e.g. append release / preview / daily) so dev and release builds never share an identifier in metadata matching.
  2. Detect and hard-fail on a collision — if a development version's UID collides with a production version's UID during build/matrix/merge, raise an error rather than silently matching.

Option 1 is the primary fix; option 2 is a useful guardrail regardless.

Acceptance criteria

  • A development/preview/daily build of version X and a release build of version X produce distinct UIDs.
  • bakery ci merge never matches dev-stream metadata onto release targets (or vice versa).
  • A dev↔release UID collision (if one can still occur) fails the run with a clear error instead of pushing.
  • Regression test covering a same-version dev + release scenario through bakery ci matrix / build / bakery ci merge.

Related

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdockerRelated to container images we producepriority/highHigh priority

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions