Skip to content

Commit 8de2d0c

Browse files
committed
Create issues for Waza skill findings
1 parent a39e295 commit 8de2d0c

3 files changed

Lines changed: 273 additions & 1 deletion

File tree

.github/workflows/catalog-check.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ on:
44
workflow_dispatch:
55
pull_request:
66

7+
permissions:
8+
contents: read
9+
issues: write
10+
pull-requests: read
11+
712
jobs:
813
check:
914
runs-on: ubuntu-latest
@@ -36,7 +41,7 @@ jobs:
3641
echo "$PWD/.tools" >> "$GITHUB_PATH"
3742
3843
- name: Validate automation scripts
39-
run: python3 -m py_compile scripts/generate_catalog.py scripts/generate_catalog_definitions.py scripts/generate_agent_catalog.py scripts/generate_release_notes.py scripts/import_external_catalog_sources.py scripts/upstream_watch.py scripts/waza_skill_quality.py
44+
run: python3 -m py_compile scripts/generate_catalog.py scripts/generate_catalog_definitions.py scripts/generate_agent_catalog.py scripts/generate_release_notes.py scripts/import_external_catalog_sources.py scripts/upstream_watch.py scripts/waza_skill_quality.py scripts/waza_github_issue.py
4045

4146
- name: Run automation regression tests
4247
run: python3 -m unittest discover -s scripts/tests -p 'test_*.py'
@@ -68,6 +73,12 @@ jobs:
6873
name: waza-skill-quality
6974
path: artifacts/waza-skill-quality/
7075

76+
- name: Create or update Waza skill quality issue
77+
if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
78+
env:
79+
GH_TOKEN: ${{ github.token }}
80+
run: python3 scripts/waza_github_issue.py
81+
7182
- name: Validate upstream watch JSON
7283
run: |
7384
python3 - <<'PY'

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Update this file when the user gives:
7575
Treat explicit frustration, swearing, sarcasm, repeated rejection, or "don't do this again" as strong signals that a durable rule should likely be captured here.
7676

7777
- Waza is a repository CI quality-check dependency for evaluating catalog skill quality. Do not add Waza as a catalog skill, package, tool entry, install bundle, or public catalog surface.
78+
- When Waza CI finds catalog skill-quality warnings, the workflow should create or update a GitHub issue with the concrete findings instead of relying only on workflow logs.
7879

7980
- The repo is moving away from the repo-authored `dotnet-*` skill-id namespace. Prefer clean canonical skill ids without the `dotnet-` prefix for repo-authored skills, and when renaming public skill ids, do a clean cutover instead of keeping backward-compatible legacy aliases unless the user explicitly asks for a compatibility bridge.
8081

scripts/waza_github_issue.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import json
6+
import os
7+
import subprocess
8+
import sys
9+
import tempfile
10+
from collections import Counter
11+
from pathlib import Path
12+
from typing import Any
13+
14+
15+
ROOT = Path(__file__).resolve().parents[1]
16+
DEFAULT_REPORT_JSON = ROOT / "artifacts" / "waza-skill-quality" / "report.json"
17+
DEFAULT_REPORT_MD = ROOT / "artifacts" / "waza-skill-quality" / "report.md"
18+
DEFAULT_TITLE = "Waza skill quality findings"
19+
DEFAULT_LABEL = "waza-skill-quality"
20+
BODY_MARKER = "<!-- waza-skill-quality-report -->"
21+
22+
23+
def parse_args() -> argparse.Namespace:
24+
parser = argparse.ArgumentParser(description="Create or update a GitHub issue for Waza skill quality findings.")
25+
parser.add_argument("--report-json", type=Path, default=DEFAULT_REPORT_JSON, help="Waza report JSON path.")
26+
parser.add_argument("--report-md", type=Path, default=DEFAULT_REPORT_MD, help="Waza report Markdown path.")
27+
parser.add_argument("--title", default=DEFAULT_TITLE, help="Issue title to create or update.")
28+
parser.add_argument("--label", default=DEFAULT_LABEL, help="Issue label used for deduplication.")
29+
parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY"), help="GitHub repository in owner/name form.")
30+
parser.add_argument("--dry-run", action="store_true", help="Print the issue body instead of calling gh.")
31+
return parser.parse_args()
32+
33+
34+
def run_gh(args: list[str], *, input_text: str | None = None) -> str:
35+
completed = subprocess.run(
36+
["gh", *args],
37+
input=input_text,
38+
text=True,
39+
capture_output=True,
40+
check=False,
41+
)
42+
if completed.returncode != 0:
43+
raise RuntimeError(
44+
f"gh command failed ({completed.returncode}): gh {' '.join(args)}\n"
45+
f"stdout:\n{completed.stdout}\n"
46+
f"stderr:\n{completed.stderr}"
47+
)
48+
return completed.stdout
49+
50+
51+
def load_report(path: Path) -> dict[str, Any] | None:
52+
if not path.exists():
53+
print(f"Waza report JSON not found at {path}; skipping issue sync.")
54+
return None
55+
return json.loads(path.read_text(encoding="utf-8"))
56+
57+
58+
def issue_summary(report: dict[str, Any]) -> tuple[Counter[str], list[dict[str, Any]]]:
59+
bad_skills = [skill for skill in report.get("skills", []) if skill.get("issues")]
60+
issue_counts: Counter[str] = Counter()
61+
for skill in bad_skills:
62+
for issue in skill.get("issues", []):
63+
issue_counts[str(issue.get("code", "unknown"))] += 1
64+
return issue_counts, bad_skills
65+
66+
67+
def run_url() -> str | None:
68+
server = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
69+
repo = os.environ.get("GITHUB_REPOSITORY")
70+
run_id = os.environ.get("GITHUB_RUN_ID")
71+
if not repo or not run_id:
72+
return None
73+
return f"{server}/{repo}/actions/runs/{run_id}"
74+
75+
76+
def build_issue_body(report: dict[str, Any], markdown_report: str) -> str:
77+
issue_counts, bad_skills = issue_summary(report)
78+
run = run_url()
79+
run_line = f"- Workflow run: {run}" if run else "- Workflow run: unavailable outside GitHub Actions"
80+
issue_breakdown = ", ".join(f"`{code}`: {count}" for code, count in issue_counts.most_common()) or "none"
81+
82+
top_rows = []
83+
for skill in bad_skills[:20]:
84+
issues = ", ".join(str(issue.get("code", "unknown")) for issue in skill.get("issues", []))
85+
top_rows.append(
86+
f"| [{skill.get('name')}]({skill.get('path')}) | {skill.get('sourceKind')} | "
87+
f"{skill.get('compliance')} | {skill.get('tokenCount')}/{skill.get('tokenLimit')} | {issues} |"
88+
)
89+
90+
top_table = "\n".join(
91+
[
92+
"| Skill | Source | Compliance | Tokens | Issues |",
93+
"| --- | --- | --- | ---: | --- |",
94+
*top_rows,
95+
]
96+
)
97+
98+
return "\n".join(
99+
[
100+
BODY_MARKER,
101+
"# Waza skill quality findings",
102+
"",
103+
"Waza found catalog skill-quality warnings in CI.",
104+
"",
105+
"## Summary",
106+
"",
107+
f"- Skills checked: `{report.get('totalSkills')}`",
108+
f"- Skills with warnings: `{report.get('badSkills')}`",
109+
f"- Repo-owned skills with warnings: `{report.get('repoBadSkills')}`",
110+
f"- Imported upstream skills with warnings: `{report.get('importedBadSkills')}`",
111+
f"- Issue breakdown: {issue_breakdown}",
112+
run_line,
113+
"- Full report artifact: `waza-skill-quality`",
114+
"",
115+
"## First Findings",
116+
"",
117+
top_table if top_rows else "No per-skill findings.",
118+
"",
119+
"## Full Report",
120+
"",
121+
markdown_report.strip(),
122+
"",
123+
]
124+
)
125+
126+
127+
def find_open_issue(repo: str, title: str, label: str) -> int | None:
128+
output = run_gh(
129+
[
130+
"issue",
131+
"list",
132+
"--repo",
133+
repo,
134+
"--state",
135+
"open",
136+
"--label",
137+
label,
138+
"--json",
139+
"number,title",
140+
"--limit",
141+
"100",
142+
]
143+
)
144+
issues = json.loads(output)
145+
for issue in issues:
146+
if issue.get("title") == title:
147+
return int(issue["number"])
148+
return None
149+
150+
151+
def ensure_label(repo: str, label: str) -> None:
152+
run_gh(
153+
[
154+
"label",
155+
"create",
156+
label,
157+
"--repo",
158+
repo,
159+
"--color",
160+
"7B3FF2",
161+
"--description",
162+
"Waza skill quality findings",
163+
"--force",
164+
]
165+
)
166+
167+
168+
def write_temp_body(body: str) -> Path:
169+
temp = tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False)
170+
with temp:
171+
temp.write(body)
172+
temp.write("\n")
173+
return Path(temp.name)
174+
175+
176+
def sync_issue(repo: str, title: str, label: str, body: str, has_findings: bool) -> None:
177+
ensure_label(repo, label)
178+
issue_number = find_open_issue(repo, title, label)
179+
180+
if not has_findings:
181+
if issue_number is None:
182+
print("No Waza findings and no open Waza issue to close.")
183+
return
184+
run_gh(
185+
[
186+
"issue",
187+
"close",
188+
str(issue_number),
189+
"--repo",
190+
repo,
191+
"--comment",
192+
"Waza skill quality is clean in the latest CI run.",
193+
]
194+
)
195+
print(f"Closed Waza issue #{issue_number}; latest report is clean.")
196+
return
197+
198+
body_path = write_temp_body(body)
199+
try:
200+
if issue_number is None:
201+
output = run_gh(
202+
[
203+
"issue",
204+
"create",
205+
"--repo",
206+
repo,
207+
"--title",
208+
title,
209+
"--label",
210+
label,
211+
"--body-file",
212+
str(body_path),
213+
]
214+
)
215+
print(output.strip())
216+
else:
217+
run_gh(
218+
[
219+
"issue",
220+
"edit",
221+
str(issue_number),
222+
"--repo",
223+
repo,
224+
"--title",
225+
title,
226+
"--body-file",
227+
str(body_path),
228+
"--add-label",
229+
label,
230+
]
231+
)
232+
print(f"Updated Waza issue #{issue_number}.")
233+
finally:
234+
body_path.unlink(missing_ok=True)
235+
236+
237+
def main() -> int:
238+
args = parse_args()
239+
if not args.repo and not args.dry_run:
240+
print("error: --repo or GITHUB_REPOSITORY is required", file=sys.stderr)
241+
return 2
242+
243+
report = load_report(args.report_json)
244+
if report is None:
245+
return 0
246+
247+
markdown_report = args.report_md.read_text(encoding="utf-8") if args.report_md.exists() else ""
248+
body = build_issue_body(report, markdown_report)
249+
has_findings = int(report.get("badSkills") or 0) > 0
250+
251+
if args.dry_run:
252+
print(body)
253+
return 0
254+
255+
sync_issue(str(args.repo), args.title, args.label, body, has_findings)
256+
return 0
257+
258+
259+
if __name__ == "__main__":
260+
raise SystemExit(main())

0 commit comments

Comments
 (0)