plugins/ci/skills/payload-snapshot/SKILL.md
Snapshot OpenShift payload data (release controller, PR diffs, comments, CI jobs, JUnit results, regression tracking) to a local directory for offline analysis
npx skillsauth add openshift-eng/ai-helpers payload-snapshotInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill downloads all data needed to analyze an OpenShift payload into a local directory tree. The resulting snapshot can be navigated entirely via file reads — no live API calls required during analysis.
Use this skill when you need to:
Python 3 (3.10 or later)
GitHub CLI (gh) — for PR diff, comment, and job data
brew install gh (macOS) or see https://cli.github.comgh auth logingh, release controller data is still fetched; PR data is skippedGoogle Cloud SDK (gcloud) — for JUnit test result download
brew install google-cloud-sdk (macOS) or see https://cloud.google.com/sdkgcloud entirely, JUnit data is skipped and the snapshot is
reported as incomplete (see Data completeness)Container runtime (podman) — for RHCOS RPMDB extraction
brew install podman on macOSregistry.ci.openshift.org)podman, RPMDB data is skipped; all other data is still collectedNetwork access to:
*.ocp.releases.ci.openshift.org (release controller)sippy.dptools.openshift.org (historical payload fallback)api.github.com (via gh CLI)storage.googleapis.com (via gcloud CLI)script_path="plugins/ci/skills/payload-snapshot/scripts/payload_snapshot.py"
# Snapshot a specific payload
python3 "$script_path" 4.22.0-0.nightly-2026-02-25-152806
# Custom output directory
python3 "$script_path" 4.22.0-0.nightly-2026-02-25-152806 --output-dir .work/snapshot
# Limit chain depth
python3 "$script_path" 4.22.0-0.nightly-2026-02-25-152806 --max-chain 5
# Skip JUnit download (faster, still generates job structure and summary)
python3 "$script_path" 4.22.0-0.nightly-2026-02-25-152806 --no-junit
# Skip RPMDB extraction
python3 "$script_path" 4.22.0-0.nightly-2026-02-25-152806 --no-rpmdb
# Force Sippy for all payload metadata (normally fallback is automatic)
python3 "$script_path" 4.22.0-0.nightly-2026-02-25-152806 --sippy
The script will:
ghpodmanThe output directory is structured for easy navigation:
payload/
<version>/
<stream>/
summary.json # START HERE — full triage data
CLAUDE.md # Imports AGENTS.md for Claude Code
AGENTS.md # Dynamic snapshot orientation doc
streams.json # All streams for this version
<tag>/ # Each payload in the chain
payload.json # Release controller API response
changelog.json # PRs that changed vs. previous payload
regressions.json # Test failure regression tracking
jobs/
blocking/
<job-name>/
job.json # Job metadata (state, URLs, GCS link, retries)
build_log.json # Error/warning lines + log tail (failed only)
junit/ # Only for failed jobs
junit_operator.xml # CI phase results
junit-aggregated.xml # Aggregated jobs only
results.json # Parsed test failures (full output)
informing/
<job-name>/
job.json # Job metadata only (no JUnit/build log)
<component>/ # e.g., machine-config-operator
prs/
<pr_number>/
code.diff # Git diff of the PR
comments.json # PR comments and reviews
jobs.json # CI check runs
rpmdb/ # RPMDB from RHCOS images
rhel-coreos/ # queryable with rpm --dbpath
rpmdb.sqlite
rhel-coreos-10/
rpmdb.sqlite
Find failed blocking jobs (with streaks):
jq '.blocking_jobs.failed_jobs[] | {name, state, streak: .streak.streak_length, pattern: .streak.failure_pattern}' payload/<version>/<stream>/summary.json
Check test failures and when they started:
jq '.[] | {test: .test_name, first_failed: .first_failed_in, payloads: .payloads_failing, jobs: .jobs}' payload/<version>/<stream>/<tag>/regressions.json
List PRs in a payload:
jq '.changeLogJson.updatedImages[].commits[] | {component: .name, pr: .pullURL, subject: .subject}' payload/<version>/<stream>/<tag>/changelog.json
Read a specific PR's diff:
cat payload/<version>/<stream>/<tag>/<component>/prs/<number>/code.diff
Check JUnit failures for a specific job:
jq '.[].name' payload/<version>/<stream>/<tag>/jobs/blocking/<job-name>/junit/results.json
Query RHCOS RPM packages (using the extracted rpmdb.sqlite):
rpm -qa --dbpath $(pwd)/payload/<version>/<stream>/<tag>/rpmdb/rhel-coreos
python3 payload_snapshot.py <payload_tag> [OPTIONS]
Positional:
payload_tag Payload tag (e.g., 4.22.0-0.nightly-2026-02-25-152806)
Options:
--output-dir DIR Base output directory (default: payload)
--max-chain N Maximum backward chain depth (default: 20)
--workers N Parallel workers for API calls (default: 8)
--no-junit Skip JUnit download and regression tracking
--no-rpmdb Skip RHCOS RPMDB extraction
--sippy Force Sippy for all payload metadata instead of using
the automatic release-controller-first fallback
--fail-on-incomplete Exit 1 if any requested data could not be collected
streams.jsonLists all available streams for the payload's version.
summary.jsonComprehensive stream-level triage data — start here. Contains:
payload_tag, phase, release_url, source, architecture, stream, versionchain_length, baseline_tag, hours_since_baselineblocking_jobs.failed_jobs[] — detailed objects with name, state, prow_url, gcs_url, and relative path job_json. May include: rhcos_version, streak (with streak_length, originating_payload, is_new_failure, failure_pattern), build_log_errors, test_failure_count, and relative paths junit_results, build_loginforming_jobs.failed_jobs[] — job name stringstest_failures.blocking[] — test_name, jobs, first_failed_in, payloads_failing, failure_message, failure_text (full, not truncated)payloads[] — per-payload entries with tag, phase, source, changelog_source, relative file paths, prs[] with component/diff/comments paths, and rhcos_changes[] with RPM diffs per RHCOS variantrhcos_rpms[] — RPMDB metadata for the target payload's RHCOS variants: tag, name, pullspec, rpmdb (relative path to rpmdb.sqlite)data_complete — true when all requested data was ultimately collected, including via a fallback after an initial read failed. false means some requested data could not be read at all.collection_errors[] — every read failure encountered. Each entry has reason, command, and optionally detail, stage, job, payload_tag, recovered. Reasons: auth, timeout, gcloud_missing, command_failed, junit_unavailable (nothing readable), junit_missing (nothing discovered), junit_unparseable (corrupt XML), junit_partial (some files unread), build_log_unavailable.
recovered: true means a fallback subsequently obtained the data. These entries are diagnostic only (useful for spotting a timeout that needs tuning) and do not make data_complete false.data_complete is false only when at least one error was not recovered.<a id="data-completeness"></a>
The CI artifact buckets are public. When gcloud has no active account it is run in anonymous mode automatically, so an unauthenticated environment still produces a complete snapshot. Authenticate only if you also need private buckets.
When a collection step fails, the affected file is not written and the corresponding summary field is omitted — it is never emitted as an empty list or a zero count. Specifically:
If JUnit could not be read for a job, results.json is not created, the
job entry has no test_failure_count and junit_results, and instead
carries junit_collection_failed: true.
An absent test_failure_count therefore means unknown, whereas 0 means
verified clean.
If some JUnit files were read and others were not, results.json holds
the real results that were obtained and the job entry adds
junit_collection_partial: true. test_failure_count is then a lower
bound, not a total.
A failed job with no readable JUnit at all — none discovered
(junit_missing), or nothing that parsed (junit_unparseable) — is left
without a count entirely. A job whose tests may never have run is unknown,
not clean. A parse failure alongside other readable files is the partial
case above, not this one.
Consumers must distinguish these. Treating unreadable data as "no test
failures" makes a broken job look like it failed for some other reason, which
misdirects root-cause analysis. Check data_complete before drawing any
conclusion from an absence of test failures. Use --fail-on-incomplete in
automation to exit non-zero rather than emit a partial snapshot.
State is persisted to collection_errors.json beside summary.json, so a
later process can tell that an existing results.json came from an incomplete
read.
AGENTS.md / CLAUDE.mdDynamic orientation document generated at snapshot time. Contains the specific payload tag, chain, failed jobs, file layout, key concepts, and summary.json schema. CLAUDE.md imports AGENTS.md via @AGENTS.md.
payload.jsonFull release controller response including blockingJobs, informingJobs, and asyncJobs with their states, Prow URLs, and retry attempt URLs.
changelog.jsonRelease controller diff response with changeLogJson.updatedImages listing every PR that changed between this payload and its predecessor. Also contains nodeImageStreams at the top level — RPM diffs for each RHCOS variant showing which packages changed between payloads.
rhcos_changes[] (in payloads[] within summary.json)Per-payload RHCOS RPM diff data extracted from the changelog's nodeImageStreams. Each entry contains:
name: Human-readable RHCOS version name (e.g., "Red Hat Enterprise Linux CoreOS 10.2")tag: RHCOS image stream tag (rhel-coreos for RHCOS 9, rhel-coreos-10 for RHCOS 10)changed: Dict of {package_name: {"old": old_version, "new": new_version}}added: Dict of newly added packages {package_name: new_version} (when present)removed: Dict of removed packages {package_name: old_version} (when present)Only variants with non-empty rpmDiff are included. When the snapshot is created from Sippy-based changelogs (which lack nodeImageStreams), this field is absent.
rpmdb/<variant>/rpmdb.sqliteThe rpmdb.sqlite file extracted from RHCOS images via podman. One directory per RHCOS variant (e.g., rpmdb/rhel-coreos/, rpmdb/rhel-coreos-10/), each containing rpmdb.sqlite. Queryable with rpm -qa --dbpath <absolute-path-to-variant-dir> or directly with sqlite3.
Extracted for every payload in the chain. Skipped when podman is unavailable or --no-rpmdb is passed.
regressions.jsonPer-payload regression tracking data. For each failing test in the target payload:
test_name: the failing testjobs: which jobs it fails infirst_failed_in: the earliest payload in the chain where it was failingpayloads_failing: how many consecutive payloads it has been failingfailure_message: the error messagefailure_text: full failure outputjob.jsonPer-job metadata including name, state, lifecycle (blocking/informing), Prow URL, GCS browser URL (gcs_url), retry count, whether it's an aggregated job, GCS bucket path, and rhcos_version.
The rhcos_version field is determined from the job name and OCP version:
rhcos9_10 — heterogeneous cluster (mixed RHCOS 9 and 10 node pools)rhcos10 — RHCOS 10 onlyrhcos9 — RHCOS 9 (explicit)rhcos9-default — no explicit fragment; defaults to RHCOS 9 for OCP 4.x installs (including major upgrades to 5.x)rhcos10-default — no explicit fragment; defaults to RHCOS 10 for OCP 5.x fresh installsbuild_log.json (failed blocking jobs only)Extracted from build-log.txt in GCS (handles gzip decompression). Contains:
total_lines: total line count of the build logerror_warning_count: number of lines matching error/warning patternserror_warning_lines[]: each with line_number and texttail_start_line, tail_lines[]: last 20% of the log for contextresults.json (in junit/ subdirectory)Parsed JUnit test failures for a specific job. Only includes failed/error tests. For aggregated jobs, includes per-run pass/fail/skip data with Prow URLs for each run.
code.diff, comments.json, jobs.jsonPR artifacts from GitHub (unchanged from previous version).
The script chains backwards from the target payload until it finds a payload where all blocking jobs succeeded. This is stricter than the Accepted phase — a payload can be force-accepted with failed blocking jobs, which does not count as a stop point.
Sippy's release tag list, sorted by release_time, is used to identify every
preceding assembled payload. If a tag is still retained, its payload details
and changelog come from the release controller. If it has been garbage
collected, the script constructs compatible payload.json and
changelog.json files from Sippy's release tags, pull requests, and job runs
APIs. A changelog that crosses a garbage-collected tag also comes from Sippy
because the release controller can no longer compute that diff.
The generated source and changelog_source fields expose this provenance.
Sippy-backed data is intentionally partial: RHCOS nodeImageStreams, async
jobs, and previousAttemptURLs are unavailable.
For terminal payloads (Accepted/Rejected), jobs showing Pending on the release controller are cross-checked against the actual Prow prowjob.json artifact to get their real state.
Aggregated jobs run the same underlying test multiple times with statistical analysis. The script:
aggregated- name prefixjunit-aggregated.xml which contains per-run pass/fail/skip data<system-out> to extract individual run URLsgh not authenticated: Prints a warning and continues without PR datagcloud not available: Warns, skips JUnit download, and records
gcloud_missing so the snapshot is reported incompletegcloud not authenticated: Reads the public buckets anonymouslyjob.json but no JUnit or build log)--workers flag controls parallelism for all subprocess calls (default 8)fetch-payloads (fetches recent payloads from the release controller)fetch-new-prs-in-payload (fetches PRs new in a specific payload)payload-analysis (analyzes a payload snapshot for revert candidates)tools
Analyze a JIRA issue and create a pull request to solve it. Use when the user wants to implement a fix or feature described in a Jira issue, push a branch, and open a draft PR.
development
Use when a deeper level of code review is requested. Multi-agent panel code review with specialist reviewers and forced runtime reproducers for all BLOCKING bug findings. Optionally posts to GitHub/GitLab as a PENDING review.
development
Review agentic documentation — verify claims locally against source code first, then use chai-bot for cross-repo and cross-functional verification
development
Use this skill when debugging a failed Prow CI job.