From 09238e12deccdceea67042fc9fdb386556b7e1ee Mon Sep 17 00:00:00 2001 From: "RHDH Build (rhdh-bot)" Date: Fri, 19 Jun 2026 13:41:52 -0400 Subject: [PATCH 1/2] Update to use x.y.z-buildid format for base image tags and return 0 for -h --- README.md | 2 +- skills/update-base-image/SKILL.md | 15 ++- .../scripts/analyze-base-images.sh | 55 +++++++++-- tests/unit/test_update_base_image.py | 98 ++++++++++++++++++- 4 files changed, 158 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 940b349..fbbb00d 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Manage Prow CI job configurations and trigger nightly E2E tests. Bump UBI / RHEC base image tags and refresh `@sha256` digests in **rhdh** and **rhdh-operator** (see [rhdh-repos](./skills/rhdh/references/rhdh-repos.md)). -- **[update-base-image](./skills/update-base-image/SKILL.md)** — Analyze and update Containerfile / Dockerfile using [rhdh-downstream](./skills/rhdh/references/rhdh-repos.md#rhdh-downstream) scripts (`build/scripts/`). Bundled `analyze-base-images.sh`; run `updateBaseImages.sh` per repo. Requires `skopeo login registry.redhat.io`. +- **[update-base-image](./skills/update-base-image/SKILL.md)** — Analyze and update Containerfile / Dockerfile using [rhdh-downstream](./skills/rhdh/references/rhdh-repos.md#rhdh-downstream) scripts (`build/scripts/`). Tags must be `major.minor-buildid` or `x.y.z-buildid`; bare numeric registry tags are ignored. Bundled `analyze-base-images.sh`; run `updateBaseImages.sh` per repo. Requires `skopeo login registry.redhat.io`. ```bash npx skills add redhat-developer/rhdh-skill --skill update-base-image diff --git a/skills/update-base-image/SKILL.md b/skills/update-base-image/SKILL.md index e39a659..1c6ec2d 100644 --- a/skills/update-base-image/SKILL.md +++ b/skills/update-base-image/SKILL.md @@ -100,7 +100,17 @@ Run **`updateBaseImages.sh` once per repo** from `$RHDH_BUILD_SCRIPTS`: | `--pr` | Opens one PR with all commits (protected branches) | | `--no-commit` | Writes file changes only; no git commit, push, or PR | -**Tag format:** RHEC tags may use `major.minor-buildid` (e.g. `9.8-1780434037`) or bare numeric build ids (e.g. `1780432632`). Analysis queries all matching registry tags via `getLatestImageTags.sh --tag .` (built-in excludes still apply). +**Tag format:** RHEC release tags must be `major.minor-buildid` (e.g. `9.8-1780434037`) or `x.y.z-buildid` (e.g. `1.2.3-1234567890`). Bare numeric build ids (e.g. `1780432632`) are **ignored** — `updateBaseImages.sh` skips those `FROM` lines with a warning. Default tag filter: `[0-9]+\.[0-9]+(\.[0-9]+)?-` (same as upstream). Override with `--tag` on the update script, or append `#regex` to the comment URL above `FROM` (e.g. `# https://registry.../ubi9/nodejs-24#^9\.8-`). + +**Optional `updateBaseImages.sh` flags:** + +| Flag | Purpose | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `-sb` / `--scripts-branch` | Branch for helper scripts (default: midstream branch); distinct from `-b` / `--sources-branch` (repo being updated) | +| `--no-sha` | Write tag only; omit `@sha256` digest suffix | +| `-p` / `--no-push` | Commit locally but do not push | +| `--check-recent-updates-only` | Report recently changed Dockerfiles without polling the registry | +| `--tag REGEX` | Override default well-formed tag filter for all images | ## Analyze without committing @@ -162,6 +172,9 @@ Stage-only lines (`FROM skeleton AS deps`) are ignored. | Missing `# https://registry...` comment | Add comment above `FROM` | | Registry not logged in | `skopeo login registry.redhat.io` | | Current tag already newest | Script skips; confirm with `getLatestImageTags.sh -n 5` | +| Bare numeric tag (e.g. `1780432632`) | Not well-formed; rewrite to `x.y-` or update script skips the line | +| Analyze vs update disagree on latest | Both use the same filter; ensure analyze output is not stale and comment `#regex` matches | +| `No well-formed x.y-z or x.y.z-z tag` | Registry returned only bare timestamps; check `--tag` or comment `#filter` on the image URL | ## UBI mismatch warnings diff --git a/skills/update-base-image/scripts/analyze-base-images.sh b/skills/update-base-image/scripts/analyze-base-images.sh index b0a1f31..e7d2c1a 100755 --- a/skills/update-base-image/scripts/analyze-base-images.sh +++ b/skills/update-base-image/scripts/analyze-base-images.sh @@ -9,18 +9,19 @@ FILES=() FIND_NAMES=(Containerfile Dockerfile) usage() { + local code="${1:-1}" echo "Usage: $0 [-s SCRIPTS_DIR] [-w WORKDIR]... [file ...]" echo " -s Directory with getLatestImageTags.sh (default: \$RHDH_BUILD_SCRIPTS)" echo " -w Repo root to scan (repeatable). Default: \$RHDH_REPO and \$RHDH_OPERATOR_REPO." echo " With no file args, scans Containerfile and Dockerfile under each -w (maxdepth 5)." - exit 1 + exit "$code" } while [[ $# -gt 0 ]]; do case "$1" in -s) RHDH_BUILD_SCRIPTS="${2%/}"; shift 2 ;; -w) WORKDIRS+=("${2%/}"); shift 2 ;; - -h|--help) usage ;; + -h|--help) usage 0 ;; -*) echo "Unknown option: $1" >&2; usage ;; *) FILES+=("$1"); shift ;; esac @@ -97,6 +98,30 @@ command -v skopeo >/dev/null 2>&1 || { echo "skopeo required" >&2; exit 1; } GLIT="${RHDH_BUILD_SCRIPTS}/getLatestImageTags.sh" UPDATE="${RHDH_BUILD_SCRIPTS}/updateBaseImages.sh" +# Match updateBaseImages.sh: x.y-timestamp or x.y.z-timestamp (not bare timestamps like 1780432632). +RHEC_TAG_VERSION_PREFIX='[0-9]+\.[0-9]+(\.[0-9]+)?-' + +is_well_formed_rhec_tag() { + [[ "$1" =~ ^${RHEC_TAG_VERSION_PREFIX}[0-9]+$ ]] +} + +parse_latest_well_formed_tag() { + local raw="$1" + local tag + tag=$(printf '%s\n' "$raw" | sed -n 's#.*:\([^@[:space:]]*\)$#\1#p' \ + | grep -E "^${RHEC_TAG_VERSION_PREFIX}[0-9]+" | sort -V | tail -1) + printf '%s' "$tag" +} + +resolve_glit_tag() { + local comment_url="${1:-}" + if [[ -n "$comment_url" ]] && [[ "$comment_url" == *"#"* ]]; then + printf '%s' "${comment_url#*#}" + else + printf '%s' "${RHEC_TAG_VERSION_PREFIX}" + fi +} + if [[ ! -x "$GLIT" ]]; then echo "getLatestImageTags.sh not found at ${GLIT}" >&2 echo "Set RHDH_BUILD_SCRIPTS to '/path/to/rhidp/rhdh/build/scripts' directory, or pass -s SCRIPTS_DIR." >&2 @@ -177,17 +202,31 @@ for cf in "${FILES[@]}"; do echo " FROM ${image_name}" echo " comment: ${last_comment:-"(none — add # https://registry.../image above FROM)"}" echo " current: ${current_tag}" + if ! is_well_formed_rhec_tag "$current_tag"; then + echo " warning: current tag is not well-formed x.y-z or x.y.z-z (updateBaseImages.sh will skip)" + fi - latest=$("$GLIT" -q -c "${image_name}" --tag . --latestNext latest 2>/dev/null | sort -V | tail -1 || true) - latest_tag="${latest##*:}" - latest_tag="${latest_tag%%@*}" + glit_tag=$(resolve_glit_tag "$last_comment") + latest_raw=$("$GLIT" -q -c "${image_name}" --tag "${glit_tag}" --latestNext latest 2>/dev/null || true) + latest_tag=$(parse_latest_well_formed_tag "$latest_raw") + + if [[ -n "$latest_tag" ]]; then + echo " latest: ${latest_tag}" + elif [[ -n "$latest_raw" ]]; then + echo " latest: (no well-formed x.y-z or x.y.z-z tag; filter: ${glit_tag})" + else + echo " latest: (query failed — check registry login)" + fi - echo " latest: ${latest_tag:-"(query failed — check registry login)"}" - if [[ -n "$latest_tag" ]] && [[ "$current_tag" != "$latest_tag" ]] \ + if [[ -z "$latest_tag" ]]; then + echo " status: SKIPPED (no well-formed tag — update script would skip)" + elif ! is_well_formed_rhec_tag "$current_tag"; then + echo " status: SKIPPED (malformed current tag — fix before update)" + elif [[ "$current_tag" != "$latest_tag" ]] \ && [[ "$(printf '%s\n' "${current_tag}" "${latest_tag}" | sort -V | tail -1)" == "${latest_tag}" ]]; then echo " status: UPDATE AVAILABLE" else - echo " status: ok (or could not compare)" + echo " status: ok" fi echo "" last_comment="" diff --git a/tests/unit/test_update_base_image.py b/tests/unit/test_update_base_image.py index cc9e73c..8c1c8a5 100644 --- a/tests/unit/test_update_base_image.py +++ b/tests/unit/test_update_base_image.py @@ -40,9 +40,9 @@ def test_script_exists(self) -> None: assert ANALYZE_SCRIPT.is_file() @pytest.mark.parametrize("flag", ["--help", "-h"]) - def test_help_prints_usage_and_exits_nonzero(self, flag: str) -> None: + def test_help_prints_usage_and_exits_zero(self, flag: str) -> None: result = _run_script(flag) - assert result.returncode != 0 + assert result.returncode == 0 assert "Usage:" in result.stdout + result.stderr def test_unknown_option_exits_nonzero(self) -> None: @@ -118,3 +118,97 @@ def test_no_containerfiles_found_exits_nonzero(self, tmp_path: Path) -> None: ) assert result.returncode != 0 assert "No Containerfiles or Dockerfiles found" in result.stdout + result.stderr + + @staticmethod + def _setup_mock_glit(scripts_dir: Path, output: str) -> None: + glit = scripts_dir / "getLatestImageTags.sh" + glit.write_text( + "#!/usr/bin/env bash\n" + "cat <<'EOF'\n" + f"{output.rstrip()}\n" + "EOF\n", + ) + glit.chmod(0o755) + + @staticmethod + def _write_containerfile(repo_dir: Path, tag: str) -> Path: + cf = repo_dir / "Containerfile" + cf.write_text( + "# https://registry.access.redhat.com/ubi9/nodejs-24\n" + f"FROM registry.access.redhat.com/ubi9/nodejs-24:{tag}@sha256:abc AS skeleton\n" + ) + return cf + + @pytest.mark.skipif(shutil.which("skopeo") is None, reason="skopeo not installed") + def test_well_formed_latest_tag_selected(self, tmp_path: Path) -> None: + scripts_dir = tmp_path / "scripts" + scripts_dir.mkdir() + self._setup_mock_glit( + scripts_dir, + "registry.access.redhat.com/ubi9/nodejs-24:1780432632\n" + "registry.access.redhat.com/ubi9/nodejs-24:9.8-1780434037", + ) + repo_dir = tmp_path / "repo" + repo_dir.mkdir() + self._write_containerfile(repo_dir, "9.8-1780430000") + + result = _run_script( + "-s", + str(scripts_dir), + "-w", + str(repo_dir), + str(repo_dir / "Containerfile"), + env=_clean_rhdh_env(), + ) + assert result.returncode == 0 + assert "latest: 9.8-1780434037" in result.stdout + assert "UPDATE AVAILABLE" in result.stdout + + @pytest.mark.skipif(shutil.which("skopeo") is None, reason="skopeo not installed") + def test_bare_numeric_current_tag_warns_and_skips(self, tmp_path: Path) -> None: + scripts_dir = tmp_path / "scripts" + scripts_dir.mkdir() + self._setup_mock_glit( + scripts_dir, + "registry.access.redhat.com/ubi9/nodejs-24:9.8-1780434037", + ) + repo_dir = tmp_path / "repo" + repo_dir.mkdir() + self._write_containerfile(repo_dir, "1780432632") + + result = _run_script( + "-s", + str(scripts_dir), + "-w", + str(repo_dir), + str(repo_dir / "Containerfile"), + env=_clean_rhdh_env(), + ) + assert result.returncode == 0 + assert "warning: current tag is not well-formed" in result.stdout + assert "SKIPPED (malformed current tag" in result.stdout + + @pytest.mark.skipif(shutil.which("skopeo") is None, reason="skopeo not installed") + def test_no_well_formed_latest_skips_update(self, tmp_path: Path) -> None: + scripts_dir = tmp_path / "scripts" + scripts_dir.mkdir() + self._setup_mock_glit( + scripts_dir, + "registry.access.redhat.com/ubi9/nodejs-24:1780432632\n" + "registry.access.redhat.com/ubi9/nodejs-24:1780439999", + ) + repo_dir = tmp_path / "repo" + repo_dir.mkdir() + self._write_containerfile(repo_dir, "9.8-1780430000") + + result = _run_script( + "-s", + str(scripts_dir), + "-w", + str(repo_dir), + str(repo_dir / "Containerfile"), + env=_clean_rhdh_env(), + ) + assert result.returncode == 0 + assert "no well-formed x.y-z or x.y.z-z tag" in result.stdout + assert "SKIPPED (no well-formed tag" in result.stdout From 9bad665205a3ee25f751978106502d0108629892 Mon Sep 17 00:00:00 2001 From: "RHDH Build (rhdh-bot)" Date: Fri, 19 Jun 2026 14:09:23 -0400 Subject: [PATCH 2/2] fix lint check --- tests/unit/test_update_base_image.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/unit/test_update_base_image.py b/tests/unit/test_update_base_image.py index 8c1c8a5..b12ff59 100644 --- a/tests/unit/test_update_base_image.py +++ b/tests/unit/test_update_base_image.py @@ -123,10 +123,7 @@ def test_no_containerfiles_found_exits_nonzero(self, tmp_path: Path) -> None: def _setup_mock_glit(scripts_dir: Path, output: str) -> None: glit = scripts_dir / "getLatestImageTags.sh" glit.write_text( - "#!/usr/bin/env bash\n" - "cat <<'EOF'\n" - f"{output.rstrip()}\n" - "EOF\n", + f"#!/usr/bin/env bash\ncat <<'EOF'\n{output.rstrip()}\nEOF\n", ) glit.chmod(0o755)