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
13 changes: 0 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -990,19 +990,6 @@ The default generated tag may be omitted by setting the 'add_build_tag' flag to
false. In this case, the 'tags' property must be specified or else an error
will occur.

When multiple build steps push to the same Docker repository, each step receives
a unique default tag that includes the step name (format: ``{default_tag}-{step_name}``).
This prevents tag collisions between steps. For example, if two steps named
``build-java17`` and ``build-java11`` both push to ``myimages/app``, they will
receive default tags like ``main-1791.Ia09cc5.M0-1661374484-build-java17`` and
``main-1791.Ia09cc5.M0-1661374484-build-java11`` respectively.

Docker images pushed to registries are recorded in the ``artifacts.json`` file.
When multiple steps push to the same repository, each step creates a separate
artifact entry keyed by ``{step_name}/{repository}`` (e.g., ``build-java17/myimages/app``).
This prevents artifact entries from overwriting each other and allows downstream
systems to distinguish between images from different build steps.

To push the image to a registry, you must add the --push argument to buildrunner.

The following is an example of simple configuration where only the repository
Expand Down
53 changes: 2 additions & 51 deletions buildrunner/config/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,53 +87,6 @@ def _add_default_tag_to_tags(config: Union[str, dict], default_tag: str) -> dict
return config


def _get_step_tag(default_tag: str, step_name: str, max_length: int = 128) -> str:
"""
Get a step-specific tag from a default tag and step name.

This function creates unique tags per build step by appending the step name to
the default tag. This is necessary when multiple build steps push to the same
Docker repository, as it prevents tag collisions (multiple build steps using the same default tag).

Args:
default_tag: The base default tag (typically a build ID or timestamp)
step_name: The name of the build step (e.g., "build-java17")
max_length: Maximum allowed tag length (default: 128, Docker image tag name character limit)

Returns:
A step-specific tag in the format "{default_tag}-{step_name}".
If the resulting tag exceeds max_length, the default_tag is truncated
to fit while preserving the step_name suffix.
"""
# Create step-specific tag by appending step name to default tag
step_tag = f"{default_tag}-{step_name}"

# If tag is within length limit, return as-is
if len(step_tag) <= max_length:
return step_tag

# Tag exceeds Docker's maximum length limit (typically 128 characters)
# Truncate the default_tag portion while preserving the step_name suffix
# This ensures the step name is always included for uniqueness
LOGGER.info(
f"Step tag {step_tag} is too long, truncating default tag by length of step name"
)

step_suffix = f"-{step_name}"

# If the step suffix is too long, truncate it
if len(step_suffix) > max_length:
step_suffix = step_suffix[:max_length]

# Calculate how much space we have for the default_tag portion
available_space = max_length - len(step_suffix)
step_tag = f"{default_tag[:available_space]}{step_suffix}"

LOGGER.info(f"Truncated step tag: {step_tag}")

return step_tag


def _set_default_tag(config: dict, default_tag: str) -> dict:
"""
Set default tag if not set for each image
Expand All @@ -149,20 +102,18 @@ def _set_default_tag(config: dict, default_tag: str) -> dict:
if not isinstance(steps, dict):
return config
for step_name, step in steps.items():
step_tag = _get_step_tag(default_tag, step_name)

for substep_name, substep in step.items():
if substep_name in ["push", "commit"]:
# Add default tag to tags list if not in the list
if isinstance(substep, list):
curr_image_infos = []
for push_config in substep:
curr_image_infos.append(
_add_default_tag_to_tags(push_config, step_tag)
_add_default_tag_to_tags(push_config, default_tag)
)
config["steps"][step_name][substep_name] = curr_image_infos
else:
curr_image_info = _add_default_tag_to_tags(substep, step_tag)
curr_image_info = _add_default_tag_to_tags(substep, default_tag)
config["steps"][step_name][substep_name] = curr_image_info
return config

Expand Down
49 changes: 45 additions & 4 deletions buildrunner/docker/multiplatform_image_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@
from buildrunner.config.models import MP_LOCAL_REGISTRY
from buildrunner.docker import get_dockerfile
from buildrunner.docker.image_info import BuiltImageInfo, BuiltTaggedImage
from buildrunner.errors import BuildRunnerConfigurationError, BuildRunnerError
from buildrunner.errors import (
BuildRunnerConfigurationError,
BuildRunnerError,
BuildRunnerProcessingError,
)
from buildrunner.docker import new_client


LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -276,10 +281,40 @@ def _build_with_inject(

@staticmethod
def _get_image_digest(image_ref: str) -> Optional[str]:
"""
Get the digest for an image with multiple fallback strategies.
"""
# Strategy 1: Try to get digest from local image first via python-on-whales
inspect_result = docker.buildx.imagetools.inspect(image_ref)
if not inspect_result or not inspect_result.config:
return None
return inspect_result.config.digest
if inspect_result and inspect_result.config:
LOGGER.info(
f"Found digest for {image_ref} from python-on-whales: {inspect_result.config.digest}"
)
return inspect_result.config.digest
LOGGER.warning(
f"Failed to inspect image {image_ref}: {inspect_result} using python-on-whales"
)

# Strategy 2: Try docker-py APIClient inspect_image (local)
try:
docker_client = new_client()
image_data = docker_client.inspect_image(image_ref)
# Extract digest from first repo digest that contains "@"
repo_digests = image_data.get("RepoDigests", [])
for repo_digest in repo_digests:
if "@" in repo_digest:
digest = repo_digest.split("@", 1)[1]
LOGGER.info(
f"Found digest for {image_ref} from docker-py inspect_image: {digest}"
)
return digest
except Exception as err:
LOGGER.warning(
f"Failed to inspect image {image_ref} using docker-py: {err}"
)

LOGGER.warning(f"Could not find digest for {image_ref}")
return None

# pylint: disable=too-many-arguments
@retry(
Expand Down Expand Up @@ -391,6 +426,12 @@ def _build_single_image(

# Retrieve the digest and put it on the queue
image_digest = self._get_image_digest(image_ref)
if image_digest is None:
raise BuildRunnerProcessingError(
f"Failed to retrieve digest for image {image_ref} after push. "
"This may indicate an issue with the registry or image metadata. "
"Check that the image was pushed successfully and the registry is accessible."
)
queue.put((image_ref, image_digest))

@staticmethod
Expand Down
8 changes: 6 additions & 2 deletions buildrunner/steprunner/tasks/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ def __init__(
step_runner,
step: StepBuild,
image_to_prepend_to_dockerfile=None,
force_legacy_builder=False,
): # pylint: disable=too-many-statements,too-many-branches,too-many-locals
super().__init__(step_runner, step)
self._docker_client = buildrunner.docker.new_client(
timeout=step_runner.build_runner.docker_timeout,
)
self.to_inject = {}
self.image_to_prepend_to_dockerfile = image_to_prepend_to_dockerfile
self.force_legacy_builder = force_legacy_builder

self._import = step.import_param
self.path = step.path
Expand Down Expand Up @@ -211,7 +213,9 @@ def run(self, context):
self.step_runner.log.write("Running docker build\n")

try:
if self.platforms or not buildrunner_config.run_config.use_legacy_builder:
if (
self.platforms or not buildrunner_config.run_config.use_legacy_builder
) and not self.force_legacy_builder:
if self.platforms and buildrunner_config.run_config.use_legacy_builder:
LOGGER.warning(
f"Ignoring use-legacy-builder. Using the legacy builder for multiplatform images {self.platforms} is not supported. "
Expand Down Expand Up @@ -262,7 +266,7 @@ def run(self, context):
)
context["mp_built_image"] = built_images
if num_built_platforms > 0:
context["image"] = built_images.native_platform_image.trunc_digest
context["image"] = built_images.native_platform_image.image_ref

else:
# Use the legacy builder
Expand Down
5 changes: 1 addition & 4 deletions buildrunner/steprunner/tasks/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,8 @@ def run(self, context): # pylint: disable=too-many-branches

# Add tagged image as artifact if this is a push and not just a commit
if not self._commit_only:
# Include step name in artifact key to prevent overwrites when multiple
# steps push to the same repository with different tags
artifact_key = f"{self.step_runner.name}/{repo.repository}"
self.step_runner.build_runner.add_artifact(
artifact_key,
repo.repository,
{
"type": "docker-image",
"docker:image": built_image_ids_str,
Expand Down
38 changes: 13 additions & 25 deletions buildrunner/steprunner/tasks/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1152,34 +1152,22 @@ def _run_post_build(self, context):
self.step_runner.log.info("Running post-build processing")
post_build = self.step.post_build
temp_tag = f"buildrunner-post-build-tag-{str(uuid.uuid4())}"
committed_image = self.runner.commit(self.step_runner.log)
python_on_whales.docker.image.tag(committed_image, temp_tag)
python_on_whales.docker.image.tag(
self.runner.commit(self.step_runner.log),
temp_tag,
)
self.images_to_remove.append(temp_tag)

# Force use of legacy builder for post-build since we're using a locally committed image
# Buildx with certain builders (like docker-container) cannot access local images
buildrunner_config = BuildRunnerConfig.get_instance()
original_use_legacy_builder = buildrunner_config.run_config.use_legacy_builder
if not original_use_legacy_builder:
self.step_runner.log.info(
"Forcing use of legacy builder for post-build step (committed image is local)"
)
buildrunner_config.run_config.use_legacy_builder = True

post_build.pull = False
try:
build_image_task = BuildBuildStepRunnerTask(
self.step_runner, post_build, image_to_prepend_to_dockerfile=temp_tag
)
_build_context = {}
build_image_task.run(_build_context)
context["run-image"] = _build_context.get("image", None)
finally:
# Restore the original builder setting
if not original_use_legacy_builder:
buildrunner_config.run_config.use_legacy_builder = (
original_use_legacy_builder
)
build_image_task = BuildBuildStepRunnerTask(
self.step_runner,
post_build,
image_to_prepend_to_dockerfile=temp_tag,
force_legacy_builder=True,
)
_build_context = {}
build_image_task.run(_build_context)
context["run-image"] = _build_context.get("image", None)

def cleanup(self, context): # pylint: disable=unused-argument
if self.runner:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "uv_build"

[project]
name = "buildrunner"
version = "3.20"
version = "3.21"
description = "Docker-based build tool"
readme = "README.rst"
requires-python = ">=3.9"
Expand Down Expand Up @@ -83,4 +83,4 @@ All Rights Reserved.

NOTICE: Adobe permits you to use, modify, and distribute this file in accordance
with the terms of the Adobe license agreement accompanying it.
"""'''
"""'''
29 changes: 0 additions & 29 deletions tests/test-files/test-multiplatform-image-tags.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion tests/test-files/test-push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ steps:
build:
# pull defaults to false
dockerfile: |
FROM adobe/buildrunner-test3:{{ BUILDRUNNER_BUILD_DOCKER_TAG }}-test-commit-list
FROM adobe/buildrunner-test3:{{ BUILDRUNNER_BUILD_DOCKER_TAG }}
run:
cmds:
# File should exist since the locally committed version is used
Expand Down
4 changes: 2 additions & 2 deletions tests/test-files/test-systemd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ steps:
# Remove the image
clean-images-1:
run:
image: {{ DOCKER_REGISTRY }}/docker:20.10-dind
image: {{ DOCKER_REGISTRY }}/docker:27-dind
cmd: docker rmi -f {{ DOCKER_REGISTRY }}/rockylinux:8.5
# Start up a container, it should NOT fail based on rockylinux:8.5 not existing
test-without-systemd:
Expand All @@ -25,7 +25,7 @@ steps:
cmd: /bin/true
clean-images-2:
run:
image: {{ DOCKER_REGISTRY }}/docker:20.10-dind
image: {{ DOCKER_REGISTRY }}/docker:27-dind
pull: false
cmd: docker rmi -f {{ DOCKER_REGISTRY }}/rockylinux:8.5 {{ DOCKER_REGISTRY }}/alpine:latest
test-service-without-systemd:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_buildrunner_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"test-general-buildx.yaml",
"test-general.yaml",
"test-push-artifact-buildx.yaml",
"test-multiplatform-image-tags.yaml",
"test-systemd.yaml",
"test-ssh-buildx.yaml",
]


Expand Down
25 changes: 22 additions & 3 deletions tests/test_config_validation/test_retagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def test_valid_config_with_buildrunner_build_tag(
buildrunner_config = BuildRunnerConfig.get_instance()
push_info = buildrunner_config.run_config.steps["build-container"].push
assert isinstance(push_info, list)
assert f"{id_string}-{build_number}-build-container" in push_info[0].tags
assert f"{id_string}-{build_number}" in push_info[0].tags


@pytest.mark.parametrize(
Expand All @@ -362,9 +362,28 @@ def test_valid_config_with_buildrunner_build_tag(
push:
repository: user1/buildrunner-test-multi-platform2
tags: [ 'latest' ]
"""
""",
"""
steps:
build-container-multi-platform:
build:
dockerfile: |
FROM {{ DOCKER_REGISTRY }}/busybox
platforms:
- linux/amd64
- linux/arm64/v8
push:
- repository: user1/buildrunner-test-multi-platform
tags: [ 'latest', '0.0.1' ]

use-built-image1:
run:
image: user1/buildrunner-test-multi-platform:{{ BUILDRUNNER_BUILD_DOCKER_TAG }}
cmd: echo "Hello World"
push: user1/buildrunner-test-multi-platform2
""",
],
ids=["buildrunner_build_tag_explict"],
ids=["buildrunner_build_tag_explict", "buildrunner_build_tag_implied"],
)
@mock.patch("buildrunner.config.DEFAULT_GLOBAL_CONFIG_FILES", [])
@mock.patch("buildrunner.detect_vcs")
Expand Down
Loading
Loading