Skip to content

fix(doozer,pyartcd): handle manifest unknown errors in sync-ci-images - #3161

Open
redhat-chai-bot wants to merge 2 commits into
openshift-eng:mainfrom
redhat-chai-bot:fix/sync-ci-images-gc-prevention-manifest-unknown
Open

fix(doozer,pyartcd): handle manifest unknown errors in sync-ci-images#3161
redhat-chai-bot wants to merge 2 commits into
openshift-eng:mainfrom
redhat-chai-bot:fix/sync-ci-images-gc-prevention-manifest-unknown

Conversation

@redhat-chai-bot

@redhat-chai-bot redhat-chai-bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes sync-ci-images pipeline failures caused by missing image manifests. The job has been broken for 10+ days across all OCP versions (4.12–5.0), with every build (#515#614) failing on manifest unknown errors.

Root Cause

PR #3025 introduced a GC-prevention mirroring step that uses cmd_assert, which crashes the entire pipeline when a manifest doesn't resolve. PR #3036 added graceful handling for the initial mirror step but not the GC-prevention step. Additionally, the PR reconciliation step (_open_reconciliation_prs) crashes on missing builder images that the mirror step already skips.

Changes

Fix 1: doozer — GC-prevention graceful handling (doozer/doozerlib/cli/images_streams.py)

  • Changed cmd_assert to cmd_gather in the GC-prevention mirror step, with manifest unknown detection
  • Warns and continues instead of crashing — the floating QCI tag is already mirrored, so the GC tag is a safety net
  • get_image_digest() now tries -o json first, falls back to text parsing for manifest lists
  • 5 new tests for get_image_digest behavior

Fix 2: pyartcd — PR reconciliation missing image handling (pyartcd/pyartcd/pipelines/sync_ci_images.py)

  • Added --ignore-missing-images to images:streams prs open command
  • The mirror step already skips missing images; PR reconciliation should be consistent

Testing

Fixes follow-up to #3025 and #3036.


@locriandev requested in Slack thread

Summary by CodeRabbit

  • Bug Fixes

    • Improved image digest detection by using structured image metadata when available, with reliable fallback handling for manifest lists and missing data.
    • CI image mirroring now continues when optional garbage-collection prevention encounters missing images, while still reporting other failures.
    • Reconciliation workflows now tolerate CI images that are not yet available instead of failing.
  • Tests

    • Added coverage for digest detection, fallback behavior, registry authentication, and missing image scenarios.

redhat-chai-bot and others added 2 commits July 15, 2026 09:17
The sync-ci-images Jenkins job has been broken for 10+ days across all
OCP versions (4.12-5.0). The failure occurs in mirror_image() when the
GC-prevention mirror step tries to use a digest obtained from
get_image_digest() with `oc image mirror ... @sha256:...`. For
multi-arch images (manifest lists), the digest returned by
`oc image info` text output is not directly resolvable, causing
"manifest unknown" errors that crash the job via cmd_assert().

Two fixes applied:

1. Make the GC-prevention mirror step graceful: replace cmd_assert()
   with cmd_gather() and handle "manifest unknown" / "not found"
   errors with a warning instead of a hard failure. The image was
   already successfully mirrored to the floating QCI tag in the
   prior step, so the GC-prevention tag is a safety net, not a hard
   requirement.

2. Improve get_image_digest() to try JSON output first (`-o json`),
   which returns a digest that works reliably for single-arch images
   with oc image mirror. Falls back to text-output parsing when JSON
   fails (which happens for manifest lists), preserving existing
   behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _open_reconciliation_prs() method calls doozer's
`images:streams prs open` without --ignore-missing-images. When builder
images like rhel-9-golang-1.25-openshift-4.22 don't yet exist in the CI
registry, doozer's check_if_upstream_image_exists() raises an exception
that crashes the pipeline with RuntimeError: PR opening failed with rc=1.

The mirror step earlier in the pipeline already gracefully skips missing
images. Adding --ignore-missing-images to the PR reconciliation step
makes it consistent, preventing the pipeline from crashing when new
builder images are defined but not yet available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openshift-ci

openshift-ci Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign fgallott for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Image digest lookup now prefers JSON with text fallback, CI imagestream mirroring tolerates missing manifests while preserving failures for other errors, and reconciliation PR creation passes --ignore-missing-images.

Changes

CI image synchronization

Layer / File(s) Summary
Digest resolution and coverage
doozer/doozerlib/cli/images_streams.py, doozer/tests/cli/test_images_streams.py
get_image_digest attempts JSON output, supports registry configuration, falls back to text parsing, and adds coverage for success and failure paths.
Tolerant CI imagestream mirroring
doozer/doozerlib/cli/images_streams.py
The GC-prevention mirror retries failures, ignores missing manifests, and raises other command errors.
Missing-image reconciliation handling
pyartcd/pyartcd/pipelines/sync_ci_images.py
Reconciliation PR creation passes --ignore-missing-images.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: lgtm

Suggested reviewers: joepvd, thegreyd, jupierce


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Ai-Attribution ⚠️ Warning Commit 633c78c uses Co-Authored-By: Claude Opus 4.6 and no Assisted-by/Generated-by trailer was found. Replace AI co-author lines with a Red Hat Assisted-by: or Generated-by: trailer for the relevant commit(s).
No-Hardcoded-Secrets ❓ Inconclusive Shell inspection is currently failing before execution, so I can’t verify the code for secret-like literals. Please rerun the check after the shell tool is available, or provide the changed file contents/diff.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main fix for missing-manifest handling in sync-ci-images across doozer and pyartcd.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Weak-Crypto ✅ Passed PR diff only adds sha256 digest handling and missing-image tolerance; no MD5/SHA1/DES/RC4/etc. or secret comparisons were introduced.
Container-Privileges ✅ Passed Changed files are Python/tests only; the diff contains no privileged/root/hostPID/hostNetwork/hostIPC/SYS_ADMIN/allowPrivilegeEscalation settings.
No-Sensitive-Data-In-Logs ✅ Passed The PR adds no new logging of secrets/PII; the new warnings only report image names and mirror stderr for missing-image cases.
No-Injection-Vectors ✅ Passed No added SQL/eval/pickle/yaml.load/os.system/shell=True sinks; new commands go through cmd_gather/cmd_gather_async, which tokenizes args and execs without a shell.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@openshift-ci

openshift-ci Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@redhat-chai-bot: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/art-pre-commit-check 633c78c link false /test art-pre-commit-check
ci/prow/security 633c78c link false /test security

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
doozer/doozerlib/cli/images_streams.py (2)

248-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Maintain consistency in ChildProcessError arguments.

Other instances in this file that raise ChildProcessError pass the full 3-tuple (rc, stdout, stderr) from cmd_gather as the second argument (e.g., line 222, line 316). Here, _ is used for stdout and only a 2-tuple (rc_gc, stderr_gc) is passed. Capturing stdout and passing the full 3-tuple keeps exception handling consistent.

♻️ Proposed refactor
-                    rc_gc, _, stderr_gc = exectools.cmd_gather(gc_mirror_cmd, retries=3, realtime=True)
+                    rc_gc, stdout_gc, stderr_gc = exectools.cmd_gather(gc_mirror_cmd, retries=3, realtime=True)
                     if rc_gc != 0:
                         if 'manifest unknown' in stderr_gc or 'not found' in stderr_gc:
                             runtime.logger.warning(
                                 f'Could not create GC-prevention tag for {upstream_entry_name}, '
                                 f'skipping: {stderr_gc.strip()}'
                             )
                             print(
                                 f'For {upstream_entry_name}, '
                                 f'GC-prevention mirror failed (manifest unknown) - continuing'
                             )
                         else:
                             raise ChildProcessError(
                                 f'Failed to create GC-prevention tag for {upstream_entry_name}: {stderr_gc}',
-                                (rc_gc, stderr_gc),
+                                (rc_gc, stdout_gc, stderr_gc),
                             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doozer/doozerlib/cli/images_streams.py` around lines 248 - 263, Update the
gc_mirror_cmd handling to capture stdout instead of discarding it, and pass the
complete (rc_gc, stdout_gc, stderr_gc) tuple as the second argument to
ChildProcessError. Keep the existing manifest/not-found warning path unchanged.

86-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix unused variables and improve JSON dictionary validation.

Static analysis correctly points out that stderr is unpacked but never used on lines 86 and 98. You can replace it with _ to resolve the warning.

Additionally, .get() does not raise a KeyError, making the exception handler ineffective for that type. To make the parsing more robust against unexpected valid JSON structures (like arrays or strings), you can verify that the parsed info is a dictionary before calling .get().

♻️ Proposed refactor
-    rc, stdout, stderr = exectools.cmd_gather(json_cmd)
+    rc, stdout, _ = exectools.cmd_gather(json_cmd)
     if rc == 0:
         try:
             info = json.loads(stdout)
-            digest = info.get('digest', '')
-            if digest:
-                return digest
-        except (json.JSONDecodeError, KeyError):
+            if isinstance(info, dict):
+                digest = info.get('digest', '')
+                if digest:
+                    return digest
+        except json.JSONDecodeError:
             pass  # Fall through to text parsing
 
     # Fallback: text output (works for manifest lists where -o json fails)
     text_cmd = f'oc image info {pullspec}{registry_flag}'
-    rc, stdout, stderr = exectools.cmd_gather(text_cmd)
+    rc, stdout, _ = exectools.cmd_gather(text_cmd)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doozer/doozerlib/cli/images_streams.py` around lines 86 - 98, Update both
exectools.cmd_gather assignments in the JSON and text fallback paths to unpack
the unused stderr value as _. In the JSON parsing block, validate that the
parsed info is a dictionary before calling get('digest'), and remove KeyError
from the exception handler while preserving the existing fallback behavior for
invalid or unexpected JSON.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@doozer/tests/cli/test_images_streams.py`:
- Around line 483-491: Update test_get_image_digest_with_registry_config to
configure cmd_gather with side_effect responses that make the JSON attempt fail
and the text fallback succeed. Assert cmd_gather was called twice and verify
--registry-config=/path/to/auth.json is present in each call’s command
arguments.

---

Nitpick comments:
In `@doozer/doozerlib/cli/images_streams.py`:
- Around line 248-263: Update the gc_mirror_cmd handling to capture stdout
instead of discarding it, and pass the complete (rc_gc, stdout_gc, stderr_gc)
tuple as the second argument to ChildProcessError. Keep the existing
manifest/not-found warning path unchanged.
- Around line 86-98: Update both exectools.cmd_gather assignments in the JSON
and text fallback paths to unpack the unused stderr value as _. In the JSON
parsing block, validate that the parsed info is a dictionary before calling
get('digest'), and remove KeyError from the exception handler while preserving
the existing fallback behavior for invalid or unexpected JSON.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift-eng/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0e422a78-78d4-4e8f-a5a1-ab225b6bf2e4

📥 Commits

Reviewing files that changed from the base of the PR and between e56ceaa and 633c78c.

📒 Files selected for processing (3)
  • doozer/doozerlib/cli/images_streams.py
  • doozer/tests/cli/test_images_streams.py
  • pyartcd/pyartcd/pipelines/sync_ci_images.py

Comment on lines +483 to +491
def test_get_image_digest_with_registry_config(mocker):
"""Test get_image_digest passes registry config to both attempts."""
mock_gather = mocker.patch('doozerlib.cli.images_streams.exectools.cmd_gather')
mock_gather.return_value = (0, '{"digest": "sha256:abc"}', '')

images_streams.get_image_digest('quay.io/openshift/ci:test', registry_config='/path/to/auth.json')

cmd = mock_gather.call_args[0][0]
assert '--registry-config=/path/to/auth.json' in cmd

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test does not verify the fallback attempt as claimed.

The docstring states that this test verifies the registry config is passed to "both attempts". However, because mock_gather.return_value is configured to simulate a successful JSON response, get_image_digest returns immediately without ever executing the text-fallback attempt.

To accurately test both attempts, configure side_effect to simulate a JSON failure followed by a text success, and then verify the arguments for both calls.

💚 Proposed fix to test both attempts
 def test_get_image_digest_with_registry_config(mocker):
     """Test get_image_digest passes registry config to both attempts."""
     mock_gather = mocker.patch('doozerlib.cli.images_streams.exectools.cmd_gather')
-    mock_gather.return_value = (0, '{"digest": "sha256:abc"}', '')
+    mock_gather.side_effect = [
+        (1, '', 'error: json failed'),
+        (0, 'Digest: sha256:abc\n', '')
+    ]
 
     images_streams.get_image_digest('quay.io/openshift/ci:test', registry_config='/path/to/auth.json')
 
-    cmd = mock_gather.call_args[0][0]
-    assert '--registry-config=/path/to/auth.json' in cmd
+    assert mock_gather.call_count == 2
+    assert '--registry-config=/path/to/auth.json' in mock_gather.call_args_list[0][0][0]
+    assert '--registry-config=/path/to/auth.json' in mock_gather.call_args_list[1][0][0]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_get_image_digest_with_registry_config(mocker):
"""Test get_image_digest passes registry config to both attempts."""
mock_gather = mocker.patch('doozerlib.cli.images_streams.exectools.cmd_gather')
mock_gather.return_value = (0, '{"digest": "sha256:abc"}', '')
images_streams.get_image_digest('quay.io/openshift/ci:test', registry_config='/path/to/auth.json')
cmd = mock_gather.call_args[0][0]
assert '--registry-config=/path/to/auth.json' in cmd
def test_get_image_digest_with_registry_config(mocker):
"""Test get_image_digest passes registry config to both attempts."""
mock_gather = mocker.patch('doozerlib.cli.images_streams.exectools.cmd_gather')
mock_gather.side_effect = [
(1, '', 'error: json failed'),
(0, 'Digest: sha256:abc\n', '')
]
images_streams.get_image_digest('quay.io/openshift/ci:test', registry_config='/path/to/auth.json')
assert mock_gather.call_count == 2
assert '--registry-config=/path/to/auth.json' in mock_gather.call_args_list[0][0][0]
assert '--registry-config=/path/to/auth.json' in mock_gather.call_args_list[1][0][0]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doozer/tests/cli/test_images_streams.py` around lines 483 - 491, Update
test_get_image_digest_with_registry_config to configure cmd_gather with
side_effect responses that make the JSON attempt fail and the text fallback
succeed. Assert cmd_gather was called twice and verify
--registry-config=/path/to/auth.json is present in each call’s command
arguments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant