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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion skills/update-base-image/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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-<buildid>` 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

Expand Down
55 changes: 47 additions & 8 deletions skills/update-base-image/scripts/analyze-base-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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=""
Expand Down
95 changes: 93 additions & 2 deletions tests/unit/test_update_base_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -118,3 +118,94 @@ 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(
f"#!/usr/bin/env bash\ncat <<'EOF'\n{output.rstrip()}\nEOF\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
Loading