Skip to content

Commit 4564336

Browse files
committed
feat(RELEASE-2015): add {{ component-incrementer }} variable
The existing {{ incrementer }} variable queries skopeo list-tags per repository independently. When a component pushes to multiple registries with different tag histories, each registry can receive a different increment value, breaking tag uniformity. The new {{ component-incrementer }} variable queries ALL repositories in the component and takes the global maximum increment across every registry before computing the next value. A per-component-prefix cache prevents redundant skopeo calls when the same tag template is used across multiple repos in the same pipeline run. Existing {{ incrementer }} behaviour is fully preserved. Also extends the push-to-external-registry e2e test to validate {{ component-incrementer }} uniformity across two repositories. Assisted-by: Cursor Signed-off-by: Happy Bhati <hbhati@redhat.com>
1 parent 88a547e commit 4564336

6 files changed

Lines changed: 421 additions & 8 deletions

File tree

integration-tests/push-to-external-registry/resources/managed/rpa.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ spec:
2626
- '{{ git_short_sha }}'
2727
- '{{ digest_sha }}'
2828
- 'v1.0.0-{{ incrementer }}'
29+
- 'v1.0.0-{{ component-incrementer }}'
2930
- '{{ oci_version }}'
31+
- url: quay.io/hacbs-release-tests/${component_name}-b
32+
tags:
33+
- 'v1.0.0-{{ component-incrementer }}'
3034
origin: ${tenant_namespace}
3135
pipeline:
3236
pipelineRef:

integration-tests/push-to-external-registry/test.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,42 @@ verify_release_contents() {
9393
failures=$((failures+1))
9494
fi
9595

96+
# Verify both repositories received the same {{ component-incrementer }} tag
97+
echo "Verifying uniform {{ component-incrementer }} tag across both repositories..."
98+
local auth_file
99+
auth_file=$(mktemp)
100+
chmod 600 "${auth_file}"
101+
trap 'rm -f "${auth_file}"' EXIT
102+
yq '. | select(.metadata.name | contains("push-")) | .data.".dockerconfigjson"' \
103+
"${SUITE_DIR}/resources/managed/secrets/managed-secrets.yaml" \
104+
| base64 -d > "${auth_file}"
105+
106+
local repo_a="quay.io/hacbs-release-tests/${component_name}"
107+
local repo_b="quay.io/hacbs-release-tests/${component_name}-b"
108+
local repo_a_tag repo_b_tag
109+
repo_a_tag=$(skopeo list-tags --retry-times 3 --authfile "${auth_file}" "docker://${repo_a}" \
110+
2>/dev/null | jq -r '.Tags[]' 2>/dev/null | grep -E '^v1\.0\.0-[0-9]+$' | sort -t- -k2 -rn | head -1 || true)
111+
repo_b_tag=$(skopeo list-tags --retry-times 3 --authfile "${auth_file}" "docker://${repo_b}" \
112+
2>/dev/null | jq -r '.Tags[]' 2>/dev/null | grep -E '^v1\.0\.0-[0-9]+$' | sort -t- -k2 -rn | head -1 || true)
113+
114+
rm -f "${auth_file}"
115+
trap - EXIT
116+
117+
echo "Repository A (${component_name}) got component-incrementer tag: ${repo_a_tag:-<not found>}"
118+
echo "Repository B (${component_name}-b) got component-incrementer tag: ${repo_b_tag:-<not found>}"
119+
120+
if [ -n "${repo_a_tag}" ] && [ -n "${repo_b_tag}" ]; then
121+
if [ "${repo_a_tag}" = "${repo_b_tag}" ]; then
122+
echo "✅️ Both repositories received the same uniform component-incrementer tag: ${repo_a_tag}"
123+
else
124+
echo "🔴 Tag mismatch! Repository A got '${repo_a_tag}' but Repository B got '${repo_b_tag}'"
125+
failures=$((failures+1))
126+
fi
127+
else
128+
echo "🔴 Could not retrieve component-incrementer tag from one or both repositories"
129+
failures=$((failures+1))
130+
fi
131+
96132
if [ "${failures}" -gt 0 ]; then
97133
echo "🔴 Test has FAILED with ${failures} failure(s)!"
98134
exit 1

tasks/managed/apply-mapping/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ This task supports variable expansion in tag values from the mapping. The curren
2121
* "{{ digest_sha }}" -> The image digest of the respective component
2222
* "{{ incrementer }}" -> Automatically finds the highest existing incremented tag in the
2323
repository and generates the next sequential tag (e.g., if the highest tag is v1.0.0-2, it will generate v1.0.0-3)
24+
* "{{ component-incrementer }}" -> Like {{ incrementer }}, but finds the highest existing tag
25+
across ALL repositories in the component and generates the next sequential tag uniformly.
26+
Use this instead of {{ incrementer }} when pushing to multiple registries to ensure every
27+
registry receives the same tag (e.g., if repo-a has v1.0.0-3 and repo-b has v1.0.0-5,
28+
both will receive v1.0.0-6).
2429
* "{{ oci_version }}" -> The version from OCI image annotations (org.opencontainers.image.version), with fallback
2530
to OCI image labels if not present in annotations (converts + to _ for tag compliance)
2631

tasks/managed/apply-mapping/apply-mapping.yaml

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ spec:
2929
* "{{ digest_sha }}" -> The image digest of the respective component
3030
* "{{ incrementer }}" -> Automatically finds the highest existing incremented tag in the
3131
repository and generates the next sequential tag (e.g., if the highest tag is v1.0.0-2, it will generate v1.0.0-3)
32+
* "{{ component-incrementer }}" -> Like {{ incrementer }}, but finds the highest existing tag
33+
across ALL repositories in the component and generates the next sequential tag uniformly.
34+
Use this instead of {{ incrementer }} when pushing to multiple registries to ensure every
35+
registry receives the same tag (e.g., if repo-a has v1.0.0-3 and repo-b has v1.0.0-5,
36+
both will receive v1.0.0-6).
3237
* "{{ oci_version }}" -> The version from OCI image annotations (org.opencontainers.image.version), with fallback
3338
to OCI image labels if not present in annotations (converts + to _ for tag compliance)
3439
@@ -228,6 +233,84 @@ spec:
228233
echo "$tag" # Return the final tag
229234
}
230235
236+
# Function to handle component-incrementer logic: finds the highest increment across
237+
# ALL repositories in a component and returns the next uniform sequential tag.
238+
# Results are cached by version_prefix to avoid redundant skopeo calls.
239+
# Expected arguments are: [tag_template, all_repos_json]
240+
component_increment_tag() {
241+
local tag_template="$1"
242+
local all_repos_json="${2:-[]}"
243+
244+
# Remove {{ component-incrementer }} placeholder to get the version prefix
245+
local version_prefix
246+
# shellcheck disable=SC2001
247+
version_prefix=$(echo "${tag_template}" | sed 's/{{ *component-incrementer *}}//g')
248+
249+
# Return cached result if available for this prefix.
250+
# Cache files survive subshell boundaries; associative arrays do not.
251+
local cache_key
252+
cache_key=$(printf '%s' "${version_prefix}" | base64 | tr -d '=\n' | tr '+/' '-_')
253+
local cache_file="${_inc_cache_dir}/${cache_key}"
254+
if [[ -f "${cache_file}" ]]; then
255+
local cached_increment
256+
cached_increment=$(< "${cache_file}")
257+
local tag
258+
# shellcheck disable=SC2001
259+
tag=$(echo "${tag_template}" | sed "s/{{ *component-incrementer *}}/${cached_increment}/g")
260+
if [[ ! "${tag}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
261+
echo "Error: Invalid tag format after substitution: ${tag}" >&2
262+
exit 1
263+
fi
264+
echo "$tag"
265+
return
266+
fi
267+
268+
# Escape version_prefix for safe use in ERE (grep -E) and sed -E.
269+
# Without escaping, a prefix like "v1.0.0-" would treat the dots as
270+
# regex wildcards, potentially matching unintended tags.
271+
local escaped_prefix
272+
# shellcheck disable=SC2016
273+
escaped_prefix=$(printf '%s' "${version_prefix}" | sed 's/[.[\\*^$()+?{|]/\\&/g')
274+
275+
# Match tags with 1–6 digit increments only. Ignore 7+ digit tags to avoid
276+
# treating short commit SHAs as incrementer values
277+
local tag_pattern="^${escaped_prefix}[0-9]{1,6}$"
278+
279+
# Find the global maximum increment across all repositories in the component
280+
local global_max=0
281+
local num_repos
282+
num_repos=$(jq 'length' <<< "$all_repos_json")
283+
for ((r = 0; r < num_repos; r++)); do
284+
local repo
285+
repo=$(jq -r --argjson r "$r" '.[$r]' <<< "$all_repos_json")
286+
local existing_tags
287+
existing_tags=$(skopeo list-tags --retry-times 3 docker://"${repo}" | jq -r '.Tags[]')
288+
local repo_max
289+
repo_max=$(echo "${existing_tags}" | { grep -E "${tag_pattern}" || true; } \
290+
| sed -E "s/^${escaped_prefix}//" | sort -nr | head -n1)
291+
# Use 10# to force decimal input preventing leading 0 from being treated as octal
292+
if [[ -n "$repo_max" ]] && [[ $((10#${repo_max})) -gt $global_max ]]; then
293+
global_max=$((10#${repo_max}))
294+
fi
295+
done
296+
297+
local increment=$((global_max + 1))
298+
299+
# Cache the result so subsequent repos in this component reuse the same value
300+
echo "${increment}" > "${cache_file}"
301+
302+
local tag
303+
# shellcheck disable=SC2001
304+
tag=$(echo "${tag_template}" | sed "s/{{ *component-incrementer *}}/${increment}/g")
305+
306+
if [[ ! "${tag}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
307+
echo "Error: Invalid tag format after substitution: ${tag}" >&2
308+
exit 1
309+
fi
310+
311+
echo "$tag"
312+
}
313+
231314
# Expected arguments are: [variable, substitute_map, labels_map]
232315
substitute() {
233316
variable=$1
@@ -261,20 +344,23 @@ spec:
261344
echo "$tags_json" | jq -c --arg ts "$timestamp_val" '. + [$ts] | unique'
262345
}
263346
264-
# Expected arguments are [tags, substitute_map, labels_map, repo]
347+
# Expected arguments are [tags, substitute_map, labels_map, repo, all_repos_json]
265348
# The tags argument is a json array
266349
translate_tags () {
267-
tags=$1
268-
substitute_map=$2
269-
labels_map=$3
270-
repo=$4
350+
local tags=$1
351+
local substitute_map=$2
352+
local labels_map=$3
353+
local repo=$4
354+
local all_repos_json="${5:-[]}"
271355
if [ "$tags" = '' ] ; then
272356
echo ''
273357
return
274358
fi
275359
276-
translated_tags='[]'
360+
local translated_tags='[]'
361+
local NUM_TAGS
277362
NUM_TAGS="$(jq 'length' <<< "${tags}")"
363+
local i tag var_name replacement
278364
for ((i = 0; i < NUM_TAGS; i++)); do
279365
tag="$(jq -r --argjson i "$i" '.[$i]' <<< "${tags}")"
280366
@@ -292,6 +378,8 @@ spec:
292378
# Handle incrementer logic
293379
if [[ "$var_name" == "incrementer" ]]; then
294380
tag=$(increment_tag "$tag" "$repo")
381+
elif [[ "$var_name" == "component-incrementer" ]]; then
382+
tag=$(component_increment_tag "$tag" "$all_repos_json")
295383
else
296384
replacement=$(substitute "$var_name" "$substitute_map" "$labels_map")
297385
if [ -z "$replacement" ]; then
@@ -423,7 +511,18 @@ spec:
423511
currentTimestamp="$(date "+%Y%m%d %T")"
424512
defaultCGWSettings=$(jq -c '.defaults.contentGateway // {}' <<< "$MAPPING")
425513
NUM_MAPPED_COMPONENTS=$(jq '.components | length' "${SNAPSHOT_SPEC_FILE}")
514+
515+
# File-based cache dir for component-incrementer results. A file-based
516+
# approach is required because component_increment_tag is invoked inside
517+
# $(...) subshells (via translate_tags), so bash associative array writes
518+
# would be lost on subshell exit. Files persist across subshell boundaries.
519+
_inc_cache_dir=$(mktemp -d)
520+
trap 'rm -rf "${_inc_cache_dir}"' EXIT
521+
426522
for ((i = 0; i < NUM_MAPPED_COMPONENTS; i++)) ; do
523+
# Clear the cache at the start of each component so that different
524+
# components with the same tag template use independent repo sets.
525+
rm -f "${_inc_cache_dir}/"* 2>/dev/null || true
427526
component=$(jq -c --argjson i "$i" '.components[$i]' "${SNAPSHOT_SPEC_FILE}")
428527
componentTags=$(jq '.componentTags // []' <<< "$component")
429528
defaultComponentTags=$(jq -n --argjson defaults "$defaultTags" --argjson componentTags \
@@ -574,6 +673,17 @@ spec:
574673
"${SNAPSHOT_SPEC_FILE}" > /tmp/temp && mv /tmp/temp "${SNAPSHOT_SPEC_FILE}"
575674
fi
576675
676+
# Build a JSON array of all repository URLs for this component.
677+
# This is used by {{ component-incrementer }} to query all repos and compute a
678+
# uniform increment value across registries.
679+
component_repos_json='[]'
680+
_ci_num_repos=$(jq '.repositories | length' <<< "$component")
681+
for ((_ci_j = 0; _ci_j < _ci_num_repos; _ci_j++)) ; do
682+
_ci_repo_url=$(jq -r --argjson j "$_ci_j" '.repositories[$j].url' <<< "$component")
683+
component_repos_json=$(jq -c --arg url "$_ci_repo_url" '. + [$url]' \
684+
<<< "$component_repos_json")
685+
done
686+
577687
NUM_REPOSITORIES=$(jq '.repositories | length' <<< "$component")
578688
for ((j = 0; j < NUM_REPOSITORIES; j++)) ; do
579689
repository=$(jq -c --argjson j "$j" '.repositories[$j]' <<< "$component")
@@ -583,7 +693,8 @@ spec:
583693
584694
allTagsPreSubstitution=$(jq -n --argjson defaults "$defaultComponentTags" --argjson repoTags \
585695
"$repoTags" '$defaults? + $repoTags? | unique')
586-
tags=$(translate_tags "${allTagsPreSubstitution}" "${substitute_map}" "${labels}" "${url}")
696+
tags=$(translate_tags "${allTagsPreSubstitution}" "${substitute_map}" "${labels}" "${url}" \
697+
"${component_repos_json}")
587698
tags=$(ensure_implicit_timestamp_value "${tags}" "${timestamp}")
588699
if [ "$(jq 'length' <<< "$tags")" -gt 0 ] ; then
589700
jq --argjson i "$i" --argjson j "$j" --argjson updatedTags "$tags" \

tasks/managed/apply-mapping/tests/mocks.sh

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,23 @@ function skopeo() {
5151
echo '{"Tags": ["1.2.1", "1.2.1-26", "1.2.1-67", "1.2.1-135", "1.2.1-1737653481", "1.2.1-1740048934", "1.2.1-1745398585"]}'
5252
return
5353
fi
54-
54+
55+
# component-incrementer test repos: repo-ci-a has max 3, repo-ci-b has max 5 → global max 5 → next 6
56+
if [[ "$*" =~ list-tags\ --retry-times\ 3\ docker://repo-ci-a ]]; then
57+
echo '{"Tags": ["v1.0.0-1", "v1.0.0-2", "v1.0.0-3"]}'
58+
return
59+
fi
60+
61+
if [[ "$*" =~ list-tags\ --retry-times\ 3\ docker://repo-ci-b ]]; then
62+
echo '{"Tags": ["v1.0.0-1", "v1.0.0-2", "v1.0.0-3", "v1.0.0-4", "v1.0.0-5"]}'
63+
return
64+
fi
65+
66+
# component-incrementer single-repo test: repo-ci-c has max 3 → next 4
67+
if [[ "$*" =~ list-tags\ --retry-times\ 3\ docker://repo-ci-c ]]; then
68+
echo '{"Tags": ["v2.0.0-1", "v2.0.0-2", "v2.0.0-3"]}'
69+
return
70+
fi
5571
# 7 digit tags: ignored by {1,6} regex, incrementer starts at 1
5672
if [[ "$*" =~ list-tags\ --retry-times\ 3\ docker://repo-leadingzero ]]; then
5773
echo '{"Tags": ["v0.7.0-0760387", "v0.7.0-0760386"]}'

0 commit comments

Comments
 (0)