# Validate Bug

> Diagnose and fix jira/invalid-bug on PRs by running the same 7 checks as the jira-lifecycle-plugin

- Skill: `openshift-eng/validate-bug` (Agent Skill)
- Install (CLI): `npx skillmds@latest add openshift-eng/validate-bug`
- Raw SKILL.md: https://api.skillmd.com/api/skills/openshift-eng/validate-bug/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: openshift-eng (https://skillmd.com/u/openshift-eng)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/openshift-eng/validate-bug

---


# JIRA Bug Validator — Implementation Guide

**IMPORTANT FOR AI**: This is a procedural skill. Execute each phase in order. Do not skip steps. Do not invent field IDs — use only the documented custom field IDs below.

This skill replicates the validation logic of the [jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin) (Prow external plugin) that applies the `jira/invalid-bug` label to PRs in OpenShift repositories.

## Custom Field Reference

| Field | Custom Field ID | JQL Name |
|-------|-----------------|----------|
| Target Version | `customfield_10855` | `"Target Version"` |
| Release Note Text | `customfield_10783` | — |
| Release Note Type | `customfield_10785` | — |
| Severity | `customfield_10840` | — |

## The 7 Validation Checks

The jira-lifecycle-plugin runs these checks in order. A bug must pass ALL checks to get `jira/valid-bug`.

| # | Check | What it validates |
|---|-------|-------------------|
| 1 | Bug is open | Status is not "Closed" |
| 2 | Target version matches branch | Bug's Target Version matches the expected version for the PR's base branch |
| 3 | Bug in valid state | Status is one of: NEW, ASSIGNED, POST |
| 4 | Release notes set | Release note text is non-empty and doesn't match the default template, OR release note type is "Release Note Not Required" |
| 5 | Dependent bug states | Each dependent (linked via "Blocks") is in an allowed state |
| 6 | Dependent bug target versions | Each dependent targets an allowed version |
| 7 | Dependents exist | At least one "Blocks" link exists to a bug in the same project |

Checks 5-7 only apply to release branches (`release-4.X`), not `main`/`master`.

## Branch-to-Version Mapping

The plugin config follows a mechanical pattern:

**Target version** (what the bug on the PR's branch must target):
- `release-4.X` → `4.X.z` (prefix match — `4.X.0` also accepted)
- `main` / `master` → `5.0.0` (prefix match — `5.0` also accepted). Update when `main` targets a new major version — check `openshift/release/core-services/jira-lifecycle-plugin/config.yaml` for the current value.

**Dependent target version** (what the "Blocks" linked bug must target):
- `release-4.X` → `4.(X+1).0` or `4.(X+1).z`

**Valid states for the bug itself** (all branches):
- `NEW`, `ASSIGNED`, `POST`

**Valid states for dependent bugs** (varies by branch — check `openshift/release/core-services/jira-lifecycle-plugin/config.yaml` for current boundaries):
- `release-4.22` and newer: `MODIFIED`, `ON_QA`, `VERIFIED`
- `release-4.6` through `release-4.21`: `VERIFIED`, `RELEASE PENDING`, `CLOSED (ERRATA)`, `CLOSED (CURRENT RELEASE)`, `CLOSED (DONE)`, `CLOSED (DONE-ERRATA)`

**Dependent bug checks** (checks 5-7):
- Required for `release-4.X` branches
- NOT required for `main` / `master` (config sets `exclude_defaults: true`)

**Release notes**:
- Required by default (`require_release_notes: true`)
- Default template text (must NOT match this): `"Cause: What actions or circumstances cause this bug to present.\r\nConsequence: What happens when the bug presents.\r\nFix: What was done to fix the bug.\r\nResult: Bug doesn't present anymore."`

## Prerequisites

- `gh` CLI authenticated with GitHub
- Jira MCP server configured (Atlassian Rovo MCP)
- `cloudId`: use `redhat.atlassian.net`

## Detailed Implementation Steps

### Phase 1: Resolve Input

**Parse the argument to determine input type:**

1. **PR URL** — matches `https://github.com/{org}/{repo}/pull/{number}`
   - Extract org, repo, and PR number from the URL
   - Run: `gh pr view {number} --repo {org}/{repo} --json title,baseRefName,state`
   - Extract JIRA key from PR title using regex: first match of `[A-Z]+-[0-9]+` (e.g., `OCPBUGS-85778`)
   - Extract base branch from `baseRefName` (e.g., `release-4.20`, `main`)

2. **PR number** — plain integer
   - Determine repo from current git remote: `gh repo view --json nameWithOwner -q .nameWithOwner`
   - Then same as PR URL flow above

3. **JIRA key** — matches `[A-Z]+-[0-9]+`
   - Require `--branch` argument (no PR context to infer branch)
   - If `--branch` not provided, error: "JIRA key input requires --branch (e.g., --branch release-4.20)"

**Validate the JIRA key project:**
- Must be a bug project: `OCPBUGS`, `DFBUGS`, or `PROJQUAY`
- If not, warn: "Project {PROJECT} is not a bug project. The jira-lifecycle-plugin only validates bugs in OCPBUGS, DFBUGS, and PROJQUAY."

**Compute expected values from branch:**
- Parse branch name to extract version number
- Compute `expected_target_version`, `expected_dependent_versions`, `valid_states`, `dependent_valid_states`, `dependents_required` using the mapping above

### Phase 2: Gather Data

**Fetch the primary bug:**

Use `mcp__atlassian__getJiraIssue`:
- `cloudId`: `redhat.atlassian.net`
- `issueIdOrKey`: the JIRA key
- `fields`: `["summary", "status", "issuetype", "fixVersions", "issuelinks", "customfield_10855", "customfield_10783", "customfield_10785", "customfield_10840", "labels", "components"]`
- `responseContentFormat`: `markdown`

**Extract key data from response:**
- `status.name` — bug status (e.g., "New", "Verified")
- `customfield_10855` — Target Version array (each entry has `.name`)
- `customfield_10783` — Release Note Text (string or ADF doc)
- `customfield_10785` — Release Note Type (object with `.value`)
- `issuelinks` — array of issue links

**Find dependent bugs:**

Filter `issuelinks` for links where:
- Link type name is `"Blocks"` AND direction is `outwardIssue` (this bug blocks the dependent)
- The linked issue's project key matches the original bug's project (e.g., both are `OCPBUGS-*`)

For each dependent found:
- Record the dependent's key, status, and priority from the link data
- If the link data doesn't include Target Version, fetch the dependent with `getJiraIssue`:
  - `fields`: `["summary", "status", "customfield_10855", "fixVersions"]`

### Phase 3: Validate

Run each check and record the result:

```text
results = []
```

**Check 1: Bug is open**
- PASS if `status.name` is NOT "Closed"
- FAIL if `status.name` is "Closed"
- Pass message: `"bug is open, matching expected state (open)"`
- Fail message: `"bug is closed, expected it to be open"`

**Check 2: Target version matches branch**
- Get target versions from `customfield_10855` (array)
- FAIL if no target version is set: `"expected the bug to target the '{expected}' version, but no target version was set"`
- FAIL if multiple target versions are set (only one allowed)
- PASS if exactly one target version and its `.name` starts with the expected prefix (e.g., `"4.20"` for `release-4.20`)
- FAIL otherwise: `"expected the bug to target either version '{expected}' or 'openshift-{expected}', but it targets '{actual}' instead"`

**Check 3: Bug in valid state**
- PASS if `status.name` (uppercased) is in `["NEW", "ASSIGNED", "POST"]`
- FAIL otherwise: `"bug is in state {actual}, which is not one of the valid states (NEW, ASSIGNED, POST)"`

**Check 4: Release notes**
- Get release note text from `customfield_10783`
  - If it's an ADF document, extract the plain text content
  - If it's a string, use directly
- Get release note type from `customfield_10785`
- PASS if release note type `.value` equals `"Release Note Not Required"`
- PASS if release note text is non-empty AND does NOT start with `"Cause: What actions or circumstances"`
- FAIL otherwise: `"release note text must be set and not match the template OR release note type must be set to 'Release Note Not Required'"`

**Check 5: Dependent bug states** (skip if `dependents_required` is false)
- For each dependent bug found via "Blocks" links:
  - PASS if dependent's `status.name` (uppercased) is in the allowed `dependent_valid_states` list
  - FAIL otherwise: `"expected dependent Jira Issue {key} to be in one of the following states: {allowed}, but it is {actual} instead"`
- If no dependents exist, this check is implicitly skipped (check 7 handles that)

**Check 6: Dependent bug target versions** (skip if `dependents_required` is false)
- For each dependent bug:
  - Get its Target Version from `customfield_10855`
  - PASS if target version starts with one of the `expected_dependent_versions` prefixes
  - FAIL otherwise: `"expected dependent Jira Issue {key} to target a version in {allowed}, but it targets '{actual}' instead"`

**Check 7: Dependents exist** (skip if `dependents_required` is false)
- PASS if at least one "Blocks" link exists to a bug in the same project
- FAIL otherwise: `"expected Jira Issue to depend on a bug targeting a version in {allowed} and in one of the following states: {allowed_states}, but no dependents were found"`

### Phase 4: Report

Render results as a markdown table:

```markdown
## Validating {JIRA_KEY} against branch {BRANCH}

**Bug**: {summary}
**Status**: {status} | **Target Version**: {target_version} | **Project**: {project}

| # | Check                          | Expected                | Actual             | Result |
|---|--------------------------------|-------------------------|--------------------| -------|
| 1 | Bug is open                    | Open                    | {status}           | {P/F}  |
| 2 | Target version                 | {expected_tv}           | {actual_tv}        | {P/F}  |
| 3 | Valid state                    | NEW/ASSIGNED/POST       | {status}           | {P/F}  |
| 4 | Release notes                  | Set (not template)      | {actual_rn}        | {P/F}  |
| 5 | Dependent bug states           | {allowed_states}        | {actual_dep_state} | {P/F}  |
| 6 | Dependent bug target version   | {allowed_dep_tv}        | {actual_dep_tv}    | {P/F}  |
| 7 | Dependents exist               | At least one            | {count} found      | {P/F}  |

Result: {N} of 7 checks passed
```

For checks that were skipped (main branch, no dependents required), show `SKIP` in the Result column.

**For each failure**, list the specific fix instruction:

```markdown
### Fixes needed

1. **Target version**: Set Target Version to `{expected}` (currently `{actual}`)
2. **Status**: Transition to NEW, ASSIGNED, or POST (currently `{actual}`)
3. **Release notes**: Set release note text or set Release Note Type to "Release Note Not Required"
4. **Dependents**: Create or link a bug in {PROJECT} targeting `{expected_dep_version}`
```

**If all checks pass:**

```markdown
All 7 checks passed. If the PR still shows `jira/invalid-bug`, comment `/jira refresh` on the PR.
```

### Phase 5: Offer Fixes

After reporting, offer to fix each failing check. List the fixes and ask for confirmation before proceeding.

**Fix: Target version** (checks 2, 6)
1. Resolve the version ID: use `mcp__atlassian__getJiraIssueTypeMetaWithFields` with `projectIdOrKey: {PROJECT}`, `issueTypeId` from the bug's `issuetype.id`, and `requiredFieldsOnly: false`
2. Find `customfield_10855` in the response fields — its `allowedValues` array contains all available versions. Match the entry whose `.name` equals the expected version string (e.g., `4.20.z`) and extract its `.id`
3. Update: `mcp__atlassian__editJiraIssue` with `fields: {"customfield_10855": [{"id": "{version_id}"}]}`

**Fix: Status** (checks 1, 3)
1. Get available transitions: `mcp__atlassian__getTransitionsForJiraIssue`
2. Find transition to a valid state (prefer "New" or "POST")
3. Transition: `mcp__atlassian__transitionJiraIssue` with the transition ID

**Fix: Release notes** (check 4)
1. Ask the user for release note text. Suggest format:
   ```text
   Cause: {what causes the bug}
   Consequence: {what happens}
   Fix: {what was done}
   Result: {the fix resolves it}
   ```
2. Update: `mcp__atlassian__editJiraIssue` with `fields: {"customfield_10783": "{text}"}`
3. Alternative: if user wants to skip release notes, set release note type:
   `fields: {"customfield_10785": {"value": "Release Note Not Required"}}`

**Fix: Missing dependents** (check 7)
1. Check if a "Blocks" linked dependent already exists in the issue's `issuelinks` (same project, outward direction)
2. If no dependent exists, offer to create one:
   - Create a new bug in the same project with:
     - Same summary (prefixed with branch info if needed)
     - Target Version set to the expected dependent version
   - Link with a "Blocks" relationship: `mcp__atlassian__createIssueLink` with `type: "Blocks"`, `inwardIssue: {original_bug}`, `outwardIssue: {new_bug}` (original bug blocks the new dependent)
   - Use `mcp__atlassian__createJiraIssue` then `mcp__atlassian__createIssueLink`
3. If a dependent exists but targets wrong version or is in wrong state, offer to fix it

**Fix: Dependent bug state** (check 5)
1. For each dependent in wrong state:
   - Get transitions: `mcp__atlassian__getTransitionsForJiraIssue` for the dependent
   - Find transition to an allowed state
   - Transition: `mcp__atlassian__transitionJiraIssue`

**After all fixes applied:**
1. Re-run all 7 checks to confirm everything passes
2. Display updated results table
3. Remind user: "Comment `/jira refresh` on the PR to have the bot re-validate."

## Error Handling

| Error | Handling |
|-------|----------|
| PR not found | Display error with PR URL, suggest checking the URL |
| No JIRA key in PR title | Display the PR title, explain the expected format: `{JIRA-KEY}: description` |
| JIRA issue not found | Check if key is correct, check project access |
| MCP tool unavailable | Fall back to `gh` CLI for GitHub data; for JIRA, display manual fix instructions |
| Branch not recognized | Warn that branch `{name}` doesn't match expected patterns, ask for `--branch` override |
| Version ID not found | Display the expected version string, instruct user to set it manually in JIRA |
| Transition not available | List available transitions, explain which one is needed and why it might be blocked |

## Notes

- The jira-lifecycle-plugin config lives at `openshift/release/core-services/jira-lifecycle-plugin/config.yaml`. If validation rules change, update the mapping in this skill.
- If a human manually adds `jira/valid-bug`, the bot will not remove it.
- For cherry-pick PRs, the plugin may auto-clone the parent JIRA bug. Check for existing "Blocks" links before creating new dependents.
- The `main` branch uses `exclude_defaults: true`, which disables dependent bug checks (5-7).

