diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 44bf61c..fae8fcf 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Orchestrator skill for RHDH plugin development - onboard, update, and maintain plugins in the Extensions Catalog", - "version": "0.3.1" + "version": "0.4.0" }, "plugins": [ { "name": "rhdh", "source": "./", "description": "Skills for RHDH plugin lifecycle management", - "version": "0.3.1", + "version": "0.4.0", "strict": true } ] diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f27dff9..342541c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "rhdh", "description": "All-in-one toolkit for Red Hat Developer Hub (RHDH). Covers plugin development, overlay management, environment setup, version compatibility, CI/CD, and RHDH ecosystem navigation.", - "version": "0.3.1", + "version": "0.4.0", "author": { "name": "RHDH Store Manager" }, diff --git a/README.md b/README.md index 6034395..4906e9a 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,16 @@ Update Konflux task digests and apply `MIGRATION.md` pipeline changes in [rhdh-p npx skills add redhat-developer/rhdh-skill --skill konflux-tekton-updates ``` +### Base image + +Bump UBI / RHEC base image tags and refresh `@sha256` digests in RHDH upstream repos (`redhat-developer/rhdh`, `redhat-developer/rhdh-operator`). + +- **[update-base-image](./skills/update-base-image/SKILL.md)** — Analyze Containerfile / Dockerfile in `redhat-developer/rhdh` and `redhat-developer/rhdh-operator` using scripts from `rhidp/rhdh/build/scripts`. Bundled `analyze-base-images.sh`; run [`updateBaseImages.sh`](https://gitlab.cee.redhat.com/rhidp/rhdh/-/blob/rhdh-1-rhel-9/build/scripts/updateBaseImages.sh) per repo. Requires `skopeo login registry.redhat.io`. + +```bash +npx skills add redhat-developer/rhdh-skill --skill update-base-image +``` + ### Local Testing Test plugins in a local RHDH instance before deploying. diff --git a/pyproject.toml b/pyproject.toml index b4cce0a..e6f9153 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rhdh-skill" -version = "0.3.1" +version = "0.4.0" description = "Claude Code skill for RHDH plugin development" readme = "README.md" license = "Apache-2.0" diff --git a/skills/update-base-image/SKILL.md b/skills/update-base-image/SKILL.md new file mode 100644 index 0000000..ef9ccbc --- /dev/null +++ b/skills/update-base-image/SKILL.md @@ -0,0 +1,175 @@ +--- +name: update-base-image +description: >- + Analyze and update Red Hat UBI / RHEC base images in Containerfile / Dockerfile using + updateBaseImages.sh and analyze-base-images.sh. Use when bumping ubi9, + nodejs-24, go-toolset, or other registry.access.redhat.com images, refreshing + @sha256 digests, scanning Containerfile FROM lines, or fixing UBI minor-version + skew in the same file. Also use when the user mentions update-base-image, + update base images, base image maintenance, RHDH release prep, or weekly + base image refresh. Scripts live in rhdh/build/scripts; scan rhdh and + rhdh-operator upstream checkouts for Containerfile / Dockerfile. +disable-model-invocation: true +--- + +# Update base images (RHDH) + +Discover latest tags from the registry, analyze Containerfiles, apply updates, and flag UBI version skew—without opening catalog.redhat.com. + +## Workspace layout + +You need **three paths** — often separate git checkouts on disk: + +| Role | Env var | Typical contents | +| ------------- | -------------------- | ---------------------------------------------- | +| Build scripts | `RHDH_BUILD_SCRIPTS` | `getLatestImageTags.sh`, `updateBaseImages.sh` | +| rhdh | `RHDH_REPO` | `build/containerfiles/Containerfile` | +| rhdh-operator | `RHDH_OPERATOR_REPO` | `Dockerfile`, `.rhdh/docker/Dockerfile` | + +Scripts ship in the **rhidp/rhdh** repository under `build/scripts/`. The `-w` target is the **upstream repo root** you want to scan and update (redhat-developer/rhdh or redhat-developer/rhdh-operator). + +Set paths before running (adjust to your machine): + +```bash +export RHDH_BUILD_SCRIPTS=/path/to/rhidp/rhdh/build/scripts +export RHDH_REPO=/path/to/redhat-developer/rhdh +export RHDH_OPERATOR_REPO=/path/to/redhat-developer/rhdh-operator +``` + +## Setup (non-optional) + +| Gate | Check | If fail | +| ------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| Scripts | `$RHDH_BUILD_SCRIPTS/getLatestImageTags.sh` is executable | Clone rhidp/rhdh and set `RHDH_BUILD_SCRIPTS` | +| Target repos | `$RHDH_REPO` and `$RHDH_OPERATOR_REPO` exist (or pass `-w` to analyze) | Clone redhat-developer/rhdh and redhat-developer/rhdh-operator; set env vars or `-w` | +| Registry auth | `skopeo inspect docker://registry.access.redhat.com/ubi9/nodejs-24:9.8 2>&1 \| head -1` succeeds | Run `skopeo login registry.redhat.io` | +| Tools | `command -v skopeo jq gh git` | Install missing tools | + +## Install this skill + +```bash +npx skills add redhat-developer/rhdh-skill --skill update-base-image +``` + +## Quick run (automated update + PR) + +Run **`updateBaseImages.sh` once per repo** from `$RHDH_BUILD_SCRIPTS`: + +```bash +"$RHDH_BUILD_SCRIPTS/updateBaseImages.sh" \ + -w "$RHDH_REPO" \ + -b release-1.y \ + -f "Containerfile Dockerfile" \ + -maxdepth 5 \ + --pr + +"$RHDH_BUILD_SCRIPTS/updateBaseImages.sh" \ + -w "$RHDH_OPERATOR_REPO" \ + -b release-1.y \ + -f "Containerfile Dockerfile" \ + -maxdepth 5 \ + --pr +``` + +**Update files only** (no commit, no push, no PR): + +```bash +"$RHDH_BUILD_SCRIPTS/updateBaseImages.sh" \ + -w "$RHDH_REPO" \ + -f "Containerfile Dockerfile" \ + -maxdepth 5 \ + -px 'e2e-tests/' -px '\.ci/' \ + --no-commit + +"$RHDH_BUILD_SCRIPTS/updateBaseImages.sh" \ + -w "$RHDH_OPERATOR_REPO" \ + -f "Containerfile Dockerfile" \ + -maxdepth 5 \ + --no-commit +``` + +**Required flags:** + +| Flag | Why | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `-maxdepth 5` | Default script value is 2; depth 2 **skips** `build/containerfiles/Containerfile`. Always pass 5 for RHDH. | +| `-w` | Upstream repo root to scan (rhdh or rhdh-operator checkout) | +| `-f "Containerfile Dockerfile"` | rhdh uses `Containerfile`; rhdh-operator uses `Dockerfile` | +| `--pr` | Opens one PR with all commits (protected branches) | +| `--no-commit` | Writes file changes only; no git commit, push, or PR | + +**Tag format:** Updates use RHEC tags like `9.8-1780434037` (`major.minor-buildid`). Bare numeric tags (e.g. `1780432632`) are ignored. Default `--tag` filter is `9\.[0-9]-`. + +## Analyze without committing + +The bundled script reads `$RHDH_BUILD_SCRIPTS`, `$RHDH_REPO`, and `$RHDH_OPERATOR_REPO` when `-w` is omitted: + +```bash +# Scan both repos (requires env vars above) +~/.agents/skills/update-base-image/scripts/analyze-base-images.sh + +# Explicit repos +~/.agents/skills/update-base-image/scripts/analyze-base-images.sh \ + -w "$RHDH_REPO" \ + -w "$RHDH_OPERATOR_REPO" + +# Single file +~/.agents/skills/update-base-image/scripts/analyze-base-images.sh \ + -w "$RHDH_REPO" \ + build/containerfiles/Containerfile +``` + +Auto-discovery finds `Containerfile` and `Dockerfile` (maxdepth 5) under each `-w` repo. For **rhdh** (`$RHDH_REPO`), paths under `e2e-tests/` and `.ci/` are skipped. + +## Containerfile requirements + +Each **registry** `FROM` must have a **comment URL** on the line above (script convention): + +```containerfile +# https://registry.access.redhat.com/ubi9/nodejs-24 +FROM registry.access.redhat.com/ubi9/nodejs-24:9.8-...@sha256:... AS skeleton + +# https://registry.access.redhat.com/ubi9/nodejs-24-minimal +FROM registry.access.redhat.com/ubi9/nodejs-24-minimal:9.8-...@sha256:... AS runner +``` + +Stage-only lines (`FROM skeleton AS deps`) are ignored. + +## Agent workflow + +1. **Verify setup gates** (scripts path, both repos, registry login, tools). +2. **Scan** with `scripts/analyze-base-images.sh` (set env vars or pass `-w`). +3. **Explain** any mismatch (e.g. `nodejs-24` on 9.8 but `nodejs-24-minimal` still on 9.7). +4. **Update** each repo: + - Prefer: `"$RHDH_BUILD_SCRIPTS/updateBaseImages.sh" -w "$RHDH_REPO" ...` and `-w "$RHDH_OPERATOR_REPO"`. + - Or: edit `FROM` lines using `current` / `latest` from analyze output. +5. **Verify** UBI minors align across all `ubi9*` images in the same file after edits. +6. **Commit** with `[skip-build] [skip-e2e]` when matching project convention. + +**Success criteria:** Every registry `FROM` in scope either matches latest tag or has a documented reason to stay pinned; no UBI minor-version skew within a single Containerfile unless intentionally documented. + +## Gotchas + +| Cause | Fix | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Scripts not in target repo | Point `RHDH_BUILD_SCRIPTS` at rhdh's `build/scripts/`, not the repo being updated | +| Only scanned one repo | Run analyze/update for both rhdh and rhdh-operator | +| rhdh e2e/ci Dockerfiles | Analyze skips `e2e-tests/` and `.ci/` under `$RHDH_REPO` | +| `-maxdepth` too low | Use `-maxdepth 5` | +| Wrong `-f` pattern | Use `-f "Containerfile Dockerfile"` when covering both repos | +| 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` | +| Wrong tag format (bare build id) | Ensure scripts use `[0-9]+\.[0-9]+-` filter; tags must match `major.minor-buildid` (e.g. `9.8-1780434037`) | + +## UBI mismatch warnings + +`updateBaseImages.sh` warns when one file has multiple UBI images with different **minor** versions (9.7 vs 9.8). `analyze-base-images.sh` prints the same check during analysis. + +## Related scripts + +| Script | Location | +| ------------------------ | ---------------------------------------------- | +| `updateBaseImages.sh` | `$RHDH_BUILD_SCRIPTS/` | +| `getLatestImageTags.sh` | `$RHDH_BUILD_SCRIPTS/` | +| `analyze-base-images.sh` | This skill's `scripts/` (installed with skill) | diff --git a/skills/update-base-image/scripts/analyze-base-images.sh b/skills/update-base-image/scripts/analyze-base-images.sh new file mode 100755 index 0000000..53b0242 --- /dev/null +++ b/skills/update-base-image/scripts/analyze-base-images.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +# Analyze Containerfile/Dockerfile base images: current FROM lines, latest RHEC tags, UBI mismatches. +# SPDX-License-Identifier: EPL-2.0 +set -euo pipefail + +RHDH_BUILD_SCRIPTS="${RHDH_BUILD_SCRIPTS:-}" +RHEC_TAG_FILTER='[0-9]+\.[0-9]+-' +WORKDIRS=() +FILES=() +FIND_NAMES=(Containerfile Dockerfile) + +usage() { + 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 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -s) RHDH_BUILD_SCRIPTS="${2%/}"; shift 2 ;; + -w) WORKDIRS+=("${2%/}"); shift 2 ;; + -h|--help) usage ;; + -*) echo "Unknown option: $1" >&2; usage ;; + *) FILES+=("$1"); shift ;; + esac +done + +if [[ ${#WORKDIRS[@]} -eq 0 ]]; then + if [[ -z "${RHDH_REPO:-}" ]] || [[ -z "${RHDH_OPERATOR_REPO:-}" ]]; then + echo "Set RHDH_REPO and RHDH_OPERATOR_REPO, or pass -w WORKDIR (repeatable)." >&2 + exit 1 + fi + WORKDIRS=("$RHDH_REPO" "$RHDH_OPERATOR_REPO") +fi + +if [[ -z "$RHDH_BUILD_SCRIPTS" ]]; then + echo "Set RHDH_BUILD_SCRIPTS to '/path/to/rhidp/rhdh/build/scripts' directory, or pass -s SCRIPTS_DIR." >&2 + exit 1 +fi + +is_valid_rhec_tag() { + [[ "$1" =~ ^[0-9]+\.[0-9]+- ]] +} + +resolve_in_workdirs() { + local f="$1" + local w cf + if [[ "$f" == /* ]] && [[ -f "$f" ]]; then + echo "$f" + return 0 + fi + for w in "${WORKDIRS[@]}"; do + cf="${w}/${f}" + if [[ -f "$cf" ]]; then + if command -v realpath >/dev/null 2>&1; then + realpath "$cf" 2>/dev/null || echo "$cf" + else + echo "$cf" + fi + return 0 + fi + done + echo "" + return 1 +} + +display_path() { + local f="$1" + local w + for w in "${WORKDIRS[@]}"; do + if [[ "$f" == "${w}/"* ]]; then + echo "${w##*/}/${f#"${w}/"}" + return + fi + done + echo "$f" +} + +canonical_path() { + local p="$1" + if command -v realpath >/dev/null 2>&1; then + realpath "$p" 2>/dev/null || echo "$p" + else + echo "$p" + fi +} + +is_rhdh_repo() { + local w="$1" + [[ -n "${RHDH_REPO:-}" ]] && [[ "$(canonical_path "$w")" == "$(canonical_path "$RHDH_REPO")" ]] +} + +is_rhdh_excluded_path() { + local f="$1" + [[ "$f" == *"/e2e-tests/"* || "$f" == *"/.ci/"* ]] +} + +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" + +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 + exit 1 +fi + +for w in "${WORKDIRS[@]}"; do + [[ -d "$w" ]] || { echo "Repo not found: ${w}" >&2; exit 1; } +done + +if [[ ${#FILES[@]} -eq 0 ]]; then + for w in "${WORKDIRS[@]}"; do + find_prune=( ! -path '*/.git/*' ! -path '*/node_modules/*' ) + if is_rhdh_repo "$w"; then + find_prune+=( ! -path '*/e2e-tests/*' ! -path '*/.ci/*' ) + fi + for name in "${FIND_NAMES[@]}"; do + while IFS= read -r f; do + FILES+=("$f") + done < <(find "${w}" -maxdepth 5 -name "${name}" \ + "${find_prune[@]}" 2>/dev/null | sort) + done + done +fi + +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "No Containerfiles or Dockerfiles found under: ${WORKDIRS[*]}" + exit 1 +fi + +RESOLVED_FILES=() +for f in "${FILES[@]}"; do + cf=$(resolve_in_workdirs "$f" || true) + if [[ -n "$cf" ]] && [[ -f "$cf" ]]; then + skip=0 + for w in "${WORKDIRS[@]}"; do + if [[ "$cf" == "${w}/"* ]] && is_rhdh_repo "$w" && is_rhdh_excluded_path "$cf"; then + echo "Skip (rhdh exclude e2e-tests/.ci): $(display_path "$cf")" >&2 + skip=1 + break + fi + done + [[ "$skip" -eq 1 ]] && continue + RESOLVED_FILES+=("$cf") + else + echo "Skip (not found): $f" >&2 + fi +done +if [[ ${#RESOLVED_FILES[@]} -eq 0 ]]; then + echo "No readable container build files after resolving paths." >&2 + exit 1 +fi +FILES=("${RESOLVED_FILES[@]}") + +echo "=== Red Hat base image analysis ===" +echo "Scripts: ${RHDH_BUILD_SCRIPTS}" +echo "Repos: ${WORKDIRS[*]}" +echo "" + +for cf in "${FILES[@]}"; do + echo "## $(display_path "$cf")" + last_comment="" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^[[:space:]]*#[[:space:]]*(https?://[^[:space:]#]+) ]]; then + last_comment="${BASH_REMATCH[1]}" + continue + fi + [[ "$line" =~ ^[[:space:]]*FROM[[:space:]]+([^[:space:]]+) ]] || continue + ref="${BASH_REMATCH[1]}" + [[ "$ref" == *":"* ]] || continue + + image_path="${ref%%@*}" + image_path="${image_path#registry.access.redhat.com/}" + image_path="${image_path#registry.redhat.io/}" + image_name="${image_path%%:*}" + current_tag="${image_path#*:}" + + echo " FROM ${image_name}" + echo " comment: ${last_comment:-"(none — add # https://registry.../image above FROM)"}" + echo " current: ${current_tag}" + + latest=$("$GLIT" -q -c "${image_name}" --tag "${RHEC_TAG_FILTER}" --latestNext latest 2>/dev/null | sort -V | tail -1 || true) + latest_tag="${latest##*:}" + latest_tag="${latest_tag%%@*}" + + if [[ -n "$latest_tag" ]] && ! is_valid_rhec_tag "$latest_tag"; then + echo " latest: (ignored invalid tag '${latest_tag}' — expected major.minor-buildid, e.g. 9.8-1780434037)" + latest_tag="" + else + echo " latest: ${latest_tag:-"(query failed — check registry login)"}" + fi + if [[ -n "$latest_tag" ]] && ! is_valid_rhec_tag "$current_tag"; then + echo " status: UPDATE AVAILABLE (current tag is not major.minor-buildid)" + elif [[ -n "$latest_tag" ]] && [[ "$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)" + fi + echo "" + last_comment="" + done < "$cf" + + ubi_versions=() + while IFS= read -r fromline; do + [[ "$fromline" =~ ^[[:space:]]*FROM[[:space:]]+([^[:space:]]+) ]] || continue + ref="${BASH_REMATCH[1]}" + [[ "$ref" =~ ubi[89] ]] || continue + tag="${ref%%@*}"; tag="${tag##*:}" + [[ "$tag" =~ ^([0-9]+\.[0-9]+) ]] && ubi_versions+=("${BASH_REMATCH[1]}") + done < <(grep -iE '^[[:space:]]*FROM[[:space:]]+' "$cf" || true) + if [[ ${#ubi_versions[@]} -ge 2 ]]; then + unique=$(printf '%s\n' "${ubi_versions[@]}" | sort -u) + if [[ $(echo "$unique" | wc -l | tr -d ' ') -gt 1 ]]; then + echo " UBI WARNING: multiple minor versions in same file: $(echo "$unique" | tr '\n' ' ')" + fi + fi + echo "" +done + +if [[ -x "$UPDATE" ]]; then + for w in "${WORKDIRS[@]}"; do + echo "Run update: ${UPDATE} -w \"${w}\" -f \"Containerfile Dockerfile\" -maxdepth 5 --pr" + done +else + echo "Run update: ${RHDH_BUILD_SCRIPTS}/updateBaseImages.sh -w -f \"Containerfile Dockerfile\" -maxdepth 5 --pr" +fi