From bbc36827ac9eb9cd58c88d80defcea37d0f5b698 Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Wed, 26 Aug 2026 08:48:05 -0400 Subject: [PATCH 1/5] fix(export-dynamic): verify registry artifacts before skipping workspace (RHDHBUGS-3635) Assisted-By: opencode Signed-off-by: Stan Lewis rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- export-dynamic/export-dynamic.sh | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/export-dynamic/export-dynamic.sh b/export-dynamic/export-dynamic.sh index ac33f6f..b0264d8 100755 --- a/export-dynamic/export-dynamic.sh +++ b/export-dynamic/export-dynamic.sh @@ -89,6 +89,44 @@ then popd > /dev/null fi +if [[ "${skipWorkspace}" == "true" ]] +then + # If we are publishing containers, verify that all expected artifacts exist in the registry and are valid before skipping + if [[ "${INPUTS_IMAGE_REPOSITORY_PREFIX}" != "" && "${INPUTS_PUSH_CONTAINER_IMAGE}" == "true" && -f "${INPUTS_PLUGINS_FILE}" ]] + then + if command -v skopeo >/dev/null 2>&1 + then + echo "Verifying published artifacts in registry for workspace before skipping..." + while IFS= read -r plugin || [[ -n "$plugin" ]] + do + # Skip empty lines + if [[ -z "${plugin// /}" ]]; then + continue + fi + # Skip commented lines + # shellcheck disable=SC2001 + if [[ "$(echo "$plugin" | sed 's/^#.*//')" == "" ]]; then + continue + fi + # shellcheck disable=SC2001 + pluginPath=$(echo "$plugin" | sed 's/^\(^[^:]*\): *\(.*\)$/\1/') + if [ -d "$pluginPath" ] && [ -f "$pluginPath/package.json" ]; then + PLUGIN_NAME=$(jq -r '.name | sub("^@"; "") | sub("[/@]"; "-")' "$pluginPath/package.json") + PLUGIN_VERSION="${INPUTS_IMAGE_TAG_PREFIX}$(jq -r '.version' "$pluginPath/package.json")" + PLUGIN_CONTAINER_TAG="${INPUTS_IMAGE_REPOSITORY_PREFIX}/${PLUGIN_NAME}:${PLUGIN_VERSION}" + echo " Checking registry for ${PLUGIN_CONTAINER_TAG}..." + if ! skopeo inspect --raw "docker://${PLUGIN_CONTAINER_TAG}" 2>/dev/null | jq -e '.annotations["io.backstage.dynamic-packages"] | @base64d | fromjson | length > 0' >/dev/null 2>&1; then + echo " Missing or invalid artifact in registry: ${PLUGIN_CONTAINER_TAG}. Workspace cannot be skipped." + skipWorkspace=false + break + fi + echo " Verified valid artifact in registry: ${PLUGIN_CONTAINER_TAG}" + fi + done < "${INPUTS_PLUGINS_FILE}" + fi + fi +fi + if [[ "${skipWorkspace}" == "true" ]] then echo "Skipping workspace since it didn't change since last published commit (${INPUTS_LAST_PUBLISH_COMMIT})" From 164415b472af0f93691acc20530fc22f13777a44 Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Wed, 26 Aug 2026 08:52:33 -0400 Subject: [PATCH 2/5] fix: flatten conditional and use double brackets for conditional test Assisted-By: opencode Signed-off-by: Stan Lewis rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- README.md | 18 +++++++++ export-dynamic/action.yaml | 5 ++- export-dynamic/export-dynamic.sh | 67 ++++++++++++++++---------------- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 7a49131..b8bf726 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,24 @@ Exports plugins as dynamic plugin archives. This should be run **after** the `ov - Exports plugins as dynamic plugin packages - Handles both frontend and backend plugins - Optional container image packaging +- Verifies registry artifacts before skipping an unchanged workspace (requires `skopeo`) + +**Environment requirements:** + +When `last-publish-commit` is provided together with `image-repository-prefix`, the action uses +`skopeo` to verify that all expected container images exist in the registry with valid dynamic +package metadata before deciding to skip an unchanged workspace. If `skopeo` is not available in +that situation, the script fails with an error rather than silently skipping verification. + +`skopeo` is **pre-installed** in both known production environments: + +| Environment | Tool availability | +|---|---| +| GitHub Actions `ubuntu-latest` (Ubuntu 24.04) | `skopeo` 1.13+ included by default | +| RHDH Konflux builder image (`builder.Containerfile`) | `skopeo` installed via `dnf` | + +If you invoke `export-dynamic.sh` directly in another environment and intend to use the +workspace-skip path with container publishing, install `skopeo` before running the script. ### override-sources diff --git a/export-dynamic/action.yaml b/export-dynamic/action.yaml index 47cf3de..94a94d4 100644 --- a/export-dynamic/action.yaml +++ b/export-dynamic/action.yaml @@ -60,7 +60,10 @@ inputs: required: false last-publish-commit: - description: Optional commit ID of the last successful publishing of plugin container images + description: > + Optional commit ID of the last successful publishing of plugin container images. + When provided together with `image-repository-prefix` and `push-container-image`, registry + artifacts are verified before skipping an unchanged workspace. Requires `skopeo` in PATH. default: "" required: false diff --git a/export-dynamic/export-dynamic.sh b/export-dynamic/export-dynamic.sh index b0264d8..00a84b0 100755 --- a/export-dynamic/export-dynamic.sh +++ b/export-dynamic/export-dynamic.sh @@ -89,42 +89,41 @@ then popd > /dev/null fi -if [[ "${skipWorkspace}" == "true" ]] +if [[ "${skipWorkspace}" == "true" && "${INPUTS_IMAGE_REPOSITORY_PREFIX}" != "" && "${INPUTS_PUSH_CONTAINER_IMAGE}" == "true" && -f "${INPUTS_PLUGINS_FILE}" ]] then - # If we are publishing containers, verify that all expected artifacts exist in the registry and are valid before skipping - if [[ "${INPUTS_IMAGE_REPOSITORY_PREFIX}" != "" && "${INPUTS_PUSH_CONTAINER_IMAGE}" == "true" && -f "${INPUTS_PLUGINS_FILE}" ]] - then - if command -v skopeo >/dev/null 2>&1 - then - echo "Verifying published artifacts in registry for workspace before skipping..." - while IFS= read -r plugin || [[ -n "$plugin" ]] - do - # Skip empty lines - if [[ -z "${plugin// /}" ]]; then - continue - fi - # Skip commented lines - # shellcheck disable=SC2001 - if [[ "$(echo "$plugin" | sed 's/^#.*//')" == "" ]]; then - continue - fi - # shellcheck disable=SC2001 - pluginPath=$(echo "$plugin" | sed 's/^\(^[^:]*\): *\(.*\)$/\1/') - if [ -d "$pluginPath" ] && [ -f "$pluginPath/package.json" ]; then - PLUGIN_NAME=$(jq -r '.name | sub("^@"; "") | sub("[/@]"; "-")' "$pluginPath/package.json") - PLUGIN_VERSION="${INPUTS_IMAGE_TAG_PREFIX}$(jq -r '.version' "$pluginPath/package.json")" - PLUGIN_CONTAINER_TAG="${INPUTS_IMAGE_REPOSITORY_PREFIX}/${PLUGIN_NAME}:${PLUGIN_VERSION}" - echo " Checking registry for ${PLUGIN_CONTAINER_TAG}..." - if ! skopeo inspect --raw "docker://${PLUGIN_CONTAINER_TAG}" 2>/dev/null | jq -e '.annotations["io.backstage.dynamic-packages"] | @base64d | fromjson | length > 0' >/dev/null 2>&1; then - echo " Missing or invalid artifact in registry: ${PLUGIN_CONTAINER_TAG}. Workspace cannot be skipped." - skipWorkspace=false - break - fi - echo " Verified valid artifact in registry: ${PLUGIN_CONTAINER_TAG}" - fi - done < "${INPUTS_PLUGINS_FILE}" - fi + if ! command -v skopeo >/dev/null 2>&1; then + echo "Error: skopeo is required to verify registry artifacts before skipping a workspace, but it was not found in PATH." >&2 + echo "Install skopeo before running this script when INPUTS_PUSH_CONTAINER_IMAGE=true and INPUTS_LAST_PUBLISH_COMMIT is set." >&2 + echo "Note: GitHub Actions ubuntu-latest runners and the RHDH Konflux builder image both provide skopeo." >&2 + exit 1 fi + echo "Verifying published artifacts in registry for workspace before skipping..." + while IFS= read -r plugin || [[ -n "$plugin" ]] + do + # Skip empty lines + if [[ -z "${plugin// /}" ]]; then + continue + fi + # Skip commented lines + # shellcheck disable=SC2001 + if [[ "$(echo "$plugin" | sed 's/^#.*//')" == "" ]]; then + continue + fi + # shellcheck disable=SC2001 + pluginPath=$(echo "$plugin" | sed 's/^\(^[^:]*\): *\(.*\)$/\1/') + if [[ -d "$pluginPath" && -f "$pluginPath/package.json" ]]; then + PLUGIN_NAME=$(jq -r '.name | sub("^@"; "") | sub("[/@]"; "-")' "$pluginPath/package.json") + PLUGIN_VERSION="${INPUTS_IMAGE_TAG_PREFIX}$(jq -r '.version' "$pluginPath/package.json")" + PLUGIN_CONTAINER_TAG="${INPUTS_IMAGE_REPOSITORY_PREFIX}/${PLUGIN_NAME}:${PLUGIN_VERSION}" + echo " Checking registry for ${PLUGIN_CONTAINER_TAG}..." + if ! skopeo inspect --raw "docker://${PLUGIN_CONTAINER_TAG}" 2>/dev/null | jq -e '.annotations["io.backstage.dynamic-packages"] | @base64d | fromjson | length > 0' >/dev/null 2>&1; then + echo " Missing or invalid artifact in registry: ${PLUGIN_CONTAINER_TAG}. Workspace cannot be skipped." + skipWorkspace=false + break + fi + echo " Verified valid artifact in registry: ${PLUGIN_CONTAINER_TAG}" + fi + done < "${INPUTS_PLUGINS_FILE}" fi if [[ "${skipWorkspace}" == "true" ]] From 6e2b5d82e609e80067e8b88d930336c82377c033 Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Wed, 23 Sep 2026 09:30:54 -0400 Subject: [PATCH 3/5] fix(export-dynamic): address review feedback on registry-artifact verification - Write WORKSPACE_SKIPPED_UNCHANGED_SINCE=false to GITHUB_OUTPUT before exiting when skopeo is missing, so the action step captures the output even though the shell step runs with `|| true`; downstream steps then correctly treat the workspace as non-skipped instead of green-lighting a broken artifact. - Remove the stray reference to a `push-container-image` input in the action.yaml description for `last-publish-commit`; that flag is hardcoded in the step env, not an action input. - Correct the README claim that the script 'fails with an error' (true when invoked directly, not when called through the action) to describe the actual observable outcome via the action. - Add a README note explaining that a `versions.json` bump on a release branch causes all workspaces to be re-exported on the next run because the tag prefix changes; skopeo then finds no images under the new prefix and the skip is bypassed for all of them. Assisted-By: opencode rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- README.md | 14 +++++++++++++- export-dynamic/action.yaml | 4 ++-- export-dynamic/export-dynamic.sh | 3 +++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b8bf726..cfc0527 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,9 @@ Exports plugins as dynamic plugin archives. This should be run **after** the `ov When `last-publish-commit` is provided together with `image-repository-prefix`, the action uses `skopeo` to verify that all expected container images exist in the registry with valid dynamic package metadata before deciding to skip an unchanged workspace. If `skopeo` is not available in -that situation, the script fails with an error rather than silently skipping verification. +that situation, the script exits non-zero and sets `WORKSPACE_SKIPPED_UNCHANGED_SINCE=false` so +that downstream steps treat the workspace as requiring export rather than silently skipping +verification. `skopeo` is **pre-installed** in both known production environments: @@ -40,6 +42,16 @@ that situation, the script fails with an error rather than silently skipping ver If you invoke `export-dynamic.sh` directly in another environment and intend to use the workspace-skip path with container publishing, install `skopeo` before running the script. +**Tag prefix changes force a full re-export:** + +The skip check looks for existing images using the tag prefix from `image-tag-prefix` (set from +`versions.json` in the overlay flow). Because `versions.json` lives outside any workspace folder, +a bump to that file does not appear in the `git merge-base` ancestor check, so every workspace +looks unchanged. However, skopeo then fails to find the images under the new prefix, which +correctly causes every workspace to be re-exported and published on that one run. This is the +intended self-healing behaviour — workspaces that were previously published under the old prefix +now also get tags under the new prefix — but it means that run will be longer than usual. + ### override-sources Applies patches and source overlays to modify plugin sources before export. This should be run **before** the `export-dynamic` action. diff --git a/export-dynamic/action.yaml b/export-dynamic/action.yaml index 94a94d4..ed2a6ed 100644 --- a/export-dynamic/action.yaml +++ b/export-dynamic/action.yaml @@ -62,8 +62,8 @@ inputs: last-publish-commit: description: > Optional commit ID of the last successful publishing of plugin container images. - When provided together with `image-repository-prefix` and `push-container-image`, registry - artifacts are verified before skipping an unchanged workspace. Requires `skopeo` in PATH. + When provided together with `image-repository-prefix`, registry artifacts are verified + before skipping an unchanged workspace. Requires `skopeo` in PATH. default: "" required: false diff --git a/export-dynamic/export-dynamic.sh b/export-dynamic/export-dynamic.sh index 00a84b0..793d88d 100755 --- a/export-dynamic/export-dynamic.sh +++ b/export-dynamic/export-dynamic.sh @@ -95,6 +95,9 @@ then echo "Error: skopeo is required to verify registry artifacts before skipping a workspace, but it was not found in PATH." >&2 echo "Install skopeo before running this script when INPUTS_PUSH_CONTAINER_IMAGE=true and INPUTS_LAST_PUBLISH_COMMIT is set." >&2 echo "Note: GitHub Actions ubuntu-latest runners and the RHDH Konflux builder image both provide skopeo." >&2 + # Write the output before exiting so the action step captures it even when the step uses `|| true`. + if [[ ! "$GITHUB_OUTPUT" ]]; then GITHUB_OUTPUT=/tmp/github_output.txt; fi + echo "WORKSPACE_SKIPPED_UNCHANGED_SINCE=false" | tee -a "$GITHUB_OUTPUT" exit 1 fi echo "Verifying published artifacts in registry for workspace before skipping..." From 6fafcc4be236997e0175782bc8a25463588b92ea Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Tue, 6 Oct 2026 14:33:46 -0400 Subject: [PATCH 4/5] fix(export-dynamic): support image indexes and add artifact verification test suite - Support multi-platform OCI image indexes and manifest lists in verify_registry_artifact by dereferencing child manifests when top-level annotations are not present. - Extract verification logic into export-dynamic/verify-registry-artifacts.sh following the pack-dist-dynamic pattern. - Add comprehensive test suite in export-dynamic/verify-registry-artifacts.test.sh and wire into test-pack-dist-dynamic workflow. Assisted-By: opencode Signed-off-by: Stan Lewis rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- .github/workflows/test-pack-dist-dynamic.yaml | 3 + export-dynamic/export-dynamic.sh | 4 +- export-dynamic/verify-registry-artifacts.sh | 40 ++++ .../verify-registry-artifacts.test.sh | 213 ++++++++++++++++++ 4 files changed, 259 insertions(+), 1 deletion(-) create mode 100755 export-dynamic/verify-registry-artifacts.sh create mode 100755 export-dynamic/verify-registry-artifacts.test.sh diff --git a/.github/workflows/test-pack-dist-dynamic.yaml b/.github/workflows/test-pack-dist-dynamic.yaml index cd2f8ed..267c80b 100644 --- a/.github/workflows/test-pack-dist-dynamic.yaml +++ b/.github/workflows/test-pack-dist-dynamic.yaml @@ -23,3 +23,6 @@ jobs: - name: Run pack-dist-dynamic tests run: bash export-dynamic/pack-dist-dynamic.test.sh + + - name: Run verify-registry-artifacts tests + run: bash export-dynamic/verify-registry-artifacts.test.sh diff --git a/export-dynamic/export-dynamic.sh b/export-dynamic/export-dynamic.sh index 793d88d..2eab29c 100755 --- a/export-dynamic/export-dynamic.sh +++ b/export-dynamic/export-dynamic.sh @@ -40,6 +40,8 @@ INPUTS_CLI_CALLER=${INPUTS_CLI_CALLER:-"npx --yes ${INPUTS_CLI_PACKAGE}@${INPUTS SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=export-dynamic/pack-dist-dynamic.sh source "${SCRIPT_DIR}/pack-dist-dynamic.sh" +# shellcheck source=export-dynamic/verify-registry-artifacts.sh +source "${SCRIPT_DIR}/verify-registry-artifacts.sh" # Check local installation first, then fall back to npx --yes (requires network) run_cli() { @@ -119,7 +121,7 @@ then PLUGIN_VERSION="${INPUTS_IMAGE_TAG_PREFIX}$(jq -r '.version' "$pluginPath/package.json")" PLUGIN_CONTAINER_TAG="${INPUTS_IMAGE_REPOSITORY_PREFIX}/${PLUGIN_NAME}:${PLUGIN_VERSION}" echo " Checking registry for ${PLUGIN_CONTAINER_TAG}..." - if ! skopeo inspect --raw "docker://${PLUGIN_CONTAINER_TAG}" 2>/dev/null | jq -e '.annotations["io.backstage.dynamic-packages"] | @base64d | fromjson | length > 0' >/dev/null 2>&1; then + if ! verify_registry_artifact "${INPUTS_IMAGE_REPOSITORY_PREFIX}" "${PLUGIN_NAME}" "${PLUGIN_VERSION}"; then echo " Missing or invalid artifact in registry: ${PLUGIN_CONTAINER_TAG}. Workspace cannot be skipped." skipWorkspace=false break diff --git a/export-dynamic/verify-registry-artifacts.sh b/export-dynamic/verify-registry-artifacts.sh new file mode 100755 index 0000000..3f45c5f --- /dev/null +++ b/export-dynamic/verify-registry-artifacts.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Verifies that a container image exists in the registry and contains valid, +# non-empty dynamic package metadata (io.backstage.dynamic-packages). +# +# Supports both single-architecture OCI manifests (e.g. GHCR) and multi-architecture +# image indexes / manifest lists (e.g. Quay/Konflux) by dereferencing child manifests. + +verify_registry_artifact() { + local repo_prefix="$1" + local plugin_name="$2" + local version="$3" + local transport="${4:-docker}" + + local target="${transport}://${repo_prefix}/${plugin_name}:${version}" + if [[ "$transport" == "dir" ]]; then + target="dir:${repo_prefix}/${plugin_name}" + fi + + local raw_manifest + raw_manifest=$(skopeo inspect --raw "${target}" 2>/dev/null) || raw_manifest="" + if [[ -n "$raw_manifest" ]]; then + # If the image is an index/manifest list without direct annotations, resolve the first child manifest + if ! echo "$raw_manifest" | jq -e '.annotations["io.backstage.dynamic-packages"]' >/dev/null 2>&1 && echo "$raw_manifest" | jq -e '.manifests != null' >/dev/null 2>&1; then + local child_digest + child_digest=$(echo "$raw_manifest" | jq -r '.manifests[0].digest // empty') + if [[ -n "$child_digest" ]]; then + local child_target="${transport}://${repo_prefix}/${plugin_name}@${child_digest}" + if [[ "$transport" == "dir" ]]; then + child_target="dir:${repo_prefix}/${plugin_name}_child" + fi + raw_manifest=$(skopeo inspect --raw "${child_target}" 2>/dev/null) || raw_manifest="" + fi + fi + fi + + if [[ -z "$raw_manifest" ]] || ! echo "$raw_manifest" | jq -e '.annotations["io.backstage.dynamic-packages"] | @base64d | fromjson | length > 0' >/dev/null 2>&1; then + return 1 + fi + return 0 +} diff --git a/export-dynamic/verify-registry-artifacts.test.sh b/export-dynamic/verify-registry-artifacts.test.sh new file mode 100755 index 0000000..0190a2e --- /dev/null +++ b/export-dynamic/verify-registry-artifacts.test.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=export-dynamic/verify-registry-artifacts.sh +source "${SCRIPT_DIR}/verify-registry-artifacts.sh" + +assert_eq() { + local description="$1" + local expected="$2" + local actual="$3" + if [[ "$expected" == "$actual" ]]; then + echo "PASS: ${description}" + else + echo "FAIL: ${description} (expected '${expected}', got '${actual}')" >&2 + exit 1 + fi +} + +WORKDIR=$(mktemp -d /tmp/verify-artifacts-test-XXXXXX) +trap 'rm -rf "$WORKDIR"' EXIT + +# --- Offline tests using dir: transport --- + +# 1. Valid single OCI manifest with dynamic-packages annotation +mkdir -p "${WORKDIR}/valid-pkg" +cat << 'JSON' > "${WORKDIR}/valid-pkg/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 2 + }, + "layers": [], + "annotations": { + "io.backstage.dynamic-packages": "W3sidGVzdC1wbHVnaW4iOnsidmVyc2lvbiI6IjEuMC4wIn19XQ==" + } +} +JSON + +res=0 +verify_registry_artifact "${WORKDIR}" "valid-pkg" "1.0.0" "dir" || res=$? +assert_eq "offline: valid manifest accepted" "0" "$res" + +# 2. Manifest with empty annotation +mkdir -p "${WORKDIR}/empty-pkg" +cat << 'JSON' > "${WORKDIR}/empty-pkg/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 2 + }, + "layers": [], + "annotations": { + "io.backstage.dynamic-packages": "" + } +} +JSON + +res=0 +verify_registry_artifact "${WORKDIR}" "empty-pkg" "1.0.0" "dir" || res=$? +assert_eq "offline: empty annotation rejected" "1" "$res" + +# 3. Manifest missing dynamic-packages annotation entirely +mkdir -p "${WORKDIR}/missing-pkg" +cat << 'JSON' > "${WORKDIR}/missing-pkg/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 2 + }, + "layers": [], + "annotations": {} +} +JSON + +res=0 +verify_registry_artifact "${WORKDIR}" "missing-pkg" "1.0.0" "dir" || res=$? +assert_eq "offline: missing annotation rejected" "1" "$res" + +# 4. Manifest with empty array annotation (W10= is base64 of "[]") +mkdir -p "${WORKDIR}/empty-arr-pkg" +cat << 'JSON' > "${WORKDIR}/empty-arr-pkg/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 2 + }, + "layers": [], + "annotations": { + "io.backstage.dynamic-packages": "W10=" + } +} +JSON + +res=0 +verify_registry_artifact "${WORKDIR}" "empty-arr-pkg" "1.0.0" "dir" || res=$? +assert_eq "offline: empty array annotation rejected" "1" "$res" + +# 5. Non-existent image/directory +res=0 +verify_registry_artifact "${WORKDIR}" "non-existent" "1.0.0" "dir" || res=$? +assert_eq "offline: non-existent target rejected" "1" "$res" + +# 6. Image index dereferencing valid child manifest +mkdir -p "${WORKDIR}/index-valid-pkg" "${WORKDIR}/index-valid-pkg_child" +cat << 'JSON' > "${WORKDIR}/index-valid-pkg/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:childvalid", + "size": 100 + } + ] +} +JSON +cat << 'JSON' > "${WORKDIR}/index-valid-pkg_child/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 2 + }, + "layers": [], + "annotations": { + "io.backstage.dynamic-packages": "W3sidGVzdC1wbHVnaW4iOnsidmVyc2lvbiI6IjEuMC4wIn19XQ==" + } +} +JSON + +res=0 +verify_registry_artifact "${WORKDIR}" "index-valid-pkg" "1.0.0" "dir" || res=$? +assert_eq "offline: image index with valid child accepted" "0" "$res" + +# 7. Image index dereferencing child manifest with empty annotation +mkdir -p "${WORKDIR}/index-empty-pkg" "${WORKDIR}/index-empty-pkg_child" +cat << 'JSON' > "${WORKDIR}/index-empty-pkg/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:childempty", + "size": 100 + } + ] +} +JSON +cat << 'JSON' > "${WORKDIR}/index-empty-pkg_child/manifest.json" +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 2 + }, + "layers": [], + "annotations": { + "io.backstage.dynamic-packages": "" + } +} +JSON + +res=0 +verify_registry_artifact "${WORKDIR}" "index-empty-pkg" "1.0.0" "dir" || res=$? +assert_eq "offline: image index with empty child annotation rejected" "1" "$res" + +# --- Online live tests against real registries (if reachable) --- +if skopeo inspect --raw docker://quay.io/rhdh/red-hat-developer-hub-backstage-plugin-scorecard-backend-module-dependabot:2.0.0--1.1.2 >/dev/null 2>&1; then + echo "Running online live registry tests..." + + # Broken Quay scorecard image (empty annotation on child manifest, from RHDHBUGS-3834) + res=0 + verify_registry_artifact "quay.io/rhdh" "red-hat-developer-hub-backstage-plugin-scorecard-backend-module-dependabot" "2.0.0--1.1.1" || res=$? + assert_eq "online: broken scorecard image on Quay rejected" "1" "$res" + + # Fixed Quay scorecard image (valid annotation on child manifest) + res=0 + verify_registry_artifact "quay.io/rhdh" "red-hat-developer-hub-backstage-plugin-scorecard-backend-module-dependabot" "2.0.0--1.1.2" || res=$? + assert_eq "online: fixed scorecard image on Quay accepted" "0" "$res" + + # Valid GHCR image (single OCI manifest) + res=0 + verify_registry_artifact "ghcr.io/redhat-developer/rhdh-plugin-export-overlays" "backstage-community-plugin-github-actions" "bs_1.54.9__1.4.0" || res=$? + assert_eq "online: valid GHCR image accepted" "0" "$res" + + # Non-existent image tag + res=0 + verify_registry_artifact "ghcr.io/redhat-developer/rhdh-plugin-export-overlays" "backstage-community-plugin-github-actions" "bs_1.54.9__99.99.99" || res=$? + assert_eq "online: non-existent tag rejected" "1" "$res" +else + echo "Skipping online tests (registry unreachable or network offline)" +fi + +echo "All verify-registry-artifacts tests passed." From 00c2bccf639037d4f7ef0ea5992d1fe3f9815763 Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Tue, 6 Oct 2026 14:37:42 -0400 Subject: [PATCH 5/5] fix(export-dynamic): merge nested conditional in verify_registry_artifact Merge collapsible if statements in verify-registry-artifacts.sh to resolve SonarCloud S1066. Assisted-By: opencode Signed-off-by: Stan Lewis rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- export-dynamic/verify-registry-artifacts.sh | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/export-dynamic/verify-registry-artifacts.sh b/export-dynamic/verify-registry-artifacts.sh index 3f45c5f..d364947 100755 --- a/export-dynamic/verify-registry-artifacts.sh +++ b/export-dynamic/verify-registry-artifacts.sh @@ -18,18 +18,16 @@ verify_registry_artifact() { local raw_manifest raw_manifest=$(skopeo inspect --raw "${target}" 2>/dev/null) || raw_manifest="" - if [[ -n "$raw_manifest" ]]; then - # If the image is an index/manifest list without direct annotations, resolve the first child manifest - if ! echo "$raw_manifest" | jq -e '.annotations["io.backstage.dynamic-packages"]' >/dev/null 2>&1 && echo "$raw_manifest" | jq -e '.manifests != null' >/dev/null 2>&1; then - local child_digest - child_digest=$(echo "$raw_manifest" | jq -r '.manifests[0].digest // empty') - if [[ -n "$child_digest" ]]; then - local child_target="${transport}://${repo_prefix}/${plugin_name}@${child_digest}" - if [[ "$transport" == "dir" ]]; then - child_target="dir:${repo_prefix}/${plugin_name}_child" - fi - raw_manifest=$(skopeo inspect --raw "${child_target}" 2>/dev/null) || raw_manifest="" + # If the image is an index/manifest list without direct annotations, resolve the first child manifest + if [[ -n "$raw_manifest" ]] && ! echo "$raw_manifest" | jq -e '.annotations["io.backstage.dynamic-packages"]' >/dev/null 2>&1 && echo "$raw_manifest" | jq -e '.manifests != null' >/dev/null 2>&1; then + local child_digest + child_digest=$(echo "$raw_manifest" | jq -r '.manifests[0].digest // empty') + if [[ -n "$child_digest" ]]; then + local child_target="${transport}://${repo_prefix}/${plugin_name}@${child_digest}" + if [[ "$transport" == "dir" ]]; then + child_target="dir:${repo_prefix}/${plugin_name}_child" fi + raw_manifest=$(skopeo inspect --raw "${child_target}" 2>/dev/null) || raw_manifest="" fi fi