---
name: generate-allure-report
model: sonnet
description: "Generate Allure-ready reports from bigpowers YAML metadata. Reads execution-status.yaml, release-plan.yaml, epic capsules, task YAMLs, cycle-times.yaml, and bug registry to produce allure-results/junit-results.xml, categories.json, and executor.json. Use when preparing progress dashboards, integrating with Allure TestOps, or generating CI reports."
---

# Generate Allure Report

Generate Allure TestOps-compatible reports from bigpowers project metadata. Produces JUnit XML for story-level test results, custom categories for filtering, and executor metadata — all in the `allure-results/` directory.

## Quick Start

```bash
bash scripts/generate-allure-report.sh
```

## What It Produces

Three files in `allure-results/`:

| File | Description |
|------|-------------|
| `junit-results.xml` | One `<testcase>` per story with `<properties>` for risk, security, WSJF, tier, wave, and status. Incomplete stories get a `<failure>` element. |
| `categories.json` | Custom Allure categories for filtering by epic, risk level (P0), and security reviews. |
| `executor.json` | Build metadata — name, type, version from release-plan.yaml, build order. |

## Data Sources

See [REFERENCE.md](REFERENCE.md)

## Verify

```bash
test -f allure-results/junit-results.xml && test -f allure-results/categories.json && test -f allure-results/executor.json
```

## Handoff

- next_skill: null (terminal skill — no downstream workflow step)

---

# generate-allure-report — Reference

## Data Sources

The script reads five YAML sources from the project:

| Source | Path | Fields Used |
|--------|------|-------------|
| Execution status | `specs/execution-status.yaml` | `epics`, `stories`, `development_status` |
| Release plan | `specs/release-plan.yaml` | `release.version`, `release.status`, `bugs` summary |
| Epic capsules | `specs/epics/**/epic.yaml` + `-tasks.yaml` | Epic metadata, task pass/fail counts |
| Cycle times | `specs/metrics/cycle-times.yaml` | Story-level `cycle_minutes`, `bcp_per_hour`, `source` |
| Bug registry | `specs/bugs/registry.yaml` | Bug counts by status and severity |

## Script Body

`scripts/generate-allure-report.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || ROOT="$(dirname "${BASH_SOURCE[0]}")/.."

mkdir -p "$ROOT/allure-results"

$PYTHON - "$ROOT" <<'PY'
import json
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

root = Path(sys.argv[1])
out = root / "allure-results"

# 1. Read execution-status.yaml
exec_status_file = root / "specs" / "execution-status.yaml"
release_plan_file = root / "specs" / "release-plan.yaml"
cycle_times_file = root / "specs" / "metrics" / "cycle-times.yaml"
bugs_registry_file = root / "specs" / "bugs" / "registry.yaml"

sys.path.insert(0, str(root / "scripts" / "lib"))
from simple_yaml import parse_simple_yaml

exec_status = parse_simple_yaml(exec_status_file.read_text()) if exec_status_file.exists() else {}
release_plan = parse_simple_yaml(release_plan_file.read_text()) if release_plan_file.exists() else {}
cycle_times = parse_simple_yaml(cycle_times_file.read_text()) if cycle_times_file.exists() else {"stories": []}
bugs_registry = parse_simple_yaml(bugs_registry_file.read_text()) if bugs_registry_file.exists() else {"bugs": []}

# Build cycle-times lookup
ct_lookup = {}
for ct in cycle_times.get("stories", []):
    if isinstance(ct, dict):
        ct_lookup[ct.get("id", "")] = ct

# 2. Build JUnit XML
stories = exec_status.get("stories", {})

# Counts for testsuite attributes
total_stories = len(stories)
incomplete = sum(1 for s in stories.values() if isinstance(s, dict) and s.get("status") != "done")

testsuite = ET.Element("testsuite", {
    "name": "bigpowers-epic-progress",
    "tests": str(total_stories),
    "failures": str(incomplete),
    "errors": "0",
    "skipped": "0",
})

for story_id in sorted(stories.keys()):
    story = stories[story_id]
    if not isinstance(story, dict):
        continue

    epic_id = story.get("epic", "unknown")
    title = story.get("title", story_id)
    bcps = story.get("bcps", 0)
    status = story.get("status", "backlog")
    risk_max = story.get("risk_max", "none")
    security_max = story.get("security_max", "none")

    # Enrich with cycle-times data
    ct_data = ct_lookup.get(story_id, {})
    cycle_minutes = ct_data.get("cycle_minutes", 0)
    bcp_per_hour = ct_data.get("bcp_per_hour", 0)

    # Time: cycle_minutes * 60 for seconds in Allure display
    time_seconds = cycle_minutes * 60.0 if cycle_minutes else 0.0

    testcase = ET.SubElement(testsuite, "testcase", {
        "classname": epic_id,
        "name": f"{story_id}: {title}",
        "time": str(round(time_seconds, 3)),
    })

    props = ET.SubElement(testcase, "properties")
    ET.SubElement(props, "property", {"name": "risk", "value": risk_max})
    ET.SubElement(props, "property", {"name": "security", "value": security_max})
    ET.SubElement(props, "property", {"name": "bcps", "value": str(bcps)})
    ET.SubElement(props, "property", {"name": "status", "value": status})
    ET.SubElement(props, "property", {"name": "bcp_per_hour", "value": str(bcp_per_hour)})
    ET.SubElement(props, "property", {"name": "lead_time_minutes", "value": str(cycle_minutes)})

    if status != "done":
        ET.SubElement(testcase, "failure", {
            "message": f"Story {story_id} is {status} [risk={risk_max}, security={security_max}]",
            "type": "StoryIncomplete"
        })

tree = ET.ElementTree(testsuite)
ET.indent(tree, space="  ")
tree.write(str(out / "junit-results.xml"), encoding="utf-8", xml_declaration=True)

# 3. Build categories.json
epics = exec_status.get("epics", {})
categories = []

for epic_id in sorted(epics.keys()):
    epic = epics[epic_id]
    if isinstance(epic, dict) and epic.get("status") != "done":
        categories.append({
            "name": f"Epic: {epic.get('title', epic_id)}",
            "matchedStatuses": ["failed"],
            "messageRegex": f".*{epic_id}:.*"
        })

categories.append({
    "name": "P0 Risk",
    "matchedStatuses": ["failed"],
    "messageRegex": ".*risk.*P0.*"
})
categories.append({
    "name": "Security Review",
    "matchedStatuses": ["failed"],
    "messageRegex": ".*security.*(?:medium|high).*"
})

# Add bug-based categories
bug_list = bugs_registry.get("bugs", [])
bug_count = len(bug_list) if isinstance(bug_list, list) else 0
open_bugs = sum(1 for b in bug_list if isinstance(b, dict) and b.get("status") not in ("fixed", "closed", None))
if open_bugs > 0:
    categories.append({
        "name": "Open Bugs",
        "matchedStatuses": ["failed"],
        "messageRegex": ".*Bug.*"
    })

(out / "categories.json").write_text(json.dumps(categories, indent=2))

# 4. Build executor.json
rl = release_plan.get("release", {}) if isinstance(release_plan.get("release"), dict) else {}
executor = {
    "name": "bigpowers",
    "type": "bigpowers",
    "buildName": rl.get("version", "unknown") if isinstance(rl, dict) else "unknown",
    "buildOrder": len(exec_status.get("development_status", {})),
}
(out / "executor.json").write_text(json.dumps(executor, indent=2))

# Summary
epic_count = len([e for e in epics.values() if isinstance(e, dict) and e.get("status") == "done"])
total_epics = len(epics)
print(f"generate-allure-report: {total_stories} stories, {epic_count}/{total_epics} epics done, {bug_count} bugs")
print(f"  -> {out}/junit-results.xml")
print(f"  -> {out}/categories.json")
print(f"  -> {out}/executor.json")
PY
```

## JUnit XML Schema

```xml
<?xml version="1.0" encoding="utf-8"?>
<testsuite name="bigpowers-epic-progress" tests="N" failures="F" errors="0" skipped="0">
  <testcase classname="e01" name="e01s01: Security slopcheck tags" time="0.75">
    <properties>
      <property name="risk" value="none"/>
      <property name="security" value="none"/>
      <property name="bcps" value="1"/>
      <property name="status" value="done"/>
      <property name="bcp_per_hour" value="1.3"/>
      <property name="lead_time_minutes" value="45"/>
    </properties>
  </testcase>
  <testcase classname="e01" name="e01s99: Some incomplete story" time="0.0">
    <properties>...</properties>
    <failure message="Story e01s99 is backlog [risk=P0, security=high]" type="StoryIncomplete"/>
  </testcase>
</testsuite>
```

## Categories JSON Schema

```json
[
  {
    "name": "Epic: Quality Core - Skill Hardening",
    "matchedStatuses": ["failed"],
    "messageRegex": ".*e45:.*"
  },
  {
    "name": "P0 Risk",
    "matchedStatuses": ["failed"],
    "messageRegex": ".*risk.*P0.*"
  },
  {
    "name": "Security Review",
    "matchedStatuses": ["failed"],
    "messageRegex": ".*security.*(?:medium|high).*"
  }
]
```

## Executor JSON Schema

```json
{
  "name": "bigpowers",
  "type": "bigpowers",
  "buildName": "2.76.2",
  "buildOrder": 400
}
```

## Example Usage

```bash
# Generate reports
bash scripts/generate-allure-report.sh

# Verify output
test -f allure-results/junit-results.xml && echo "JUnit OK"
test -f allure-results/categories.json && echo "Categories OK"
test -f allure-results/executor.json && echo "Executor OK"

# Serve with Allure
allure serve allure-results/

# Or open the Allure TestOps UI
allure open allure-results/
```
