chore: ruff format
This commit is contained in:
@@ -164,7 +164,10 @@ class CreateSkillPayloadTool(NeoSkillToolBase):
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"payload": {
|
"payload": {
|
||||||
"anyOf": [{"type": "object"}, {"type": "array", "items": {"type": "object"}}],
|
"anyOf": [
|
||||||
|
{"type": "object"},
|
||||||
|
{"type": "array", "items": {"type": "object"}},
|
||||||
|
],
|
||||||
"description": (
|
"description": (
|
||||||
"Skill payload JSON. Typical schema: {skill_markdown, inputs, outputs, meta}. "
|
"Skill payload JSON. Typical schema: {skill_markdown, inputs, outputs, meta}. "
|
||||||
"This only stores content and returns payload_ref; it does not create a candidate or release."
|
"This only stores content and returns payload_ref; it does not create a candidate or release."
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Issue:
|
||||||
|
number: int
|
||||||
|
title: str
|
||||||
|
created_at: datetime
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Close duplicate open plugin-publish issues while keeping the latest one."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo",
|
||||||
|
default="AstrBotDevs/AstrBot",
|
||||||
|
help="GitHub repository in owner/name format.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--label",
|
||||||
|
default="plugin-publish",
|
||||||
|
help="Issue label to target.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--limit",
|
||||||
|
type=int,
|
||||||
|
default=1000,
|
||||||
|
help="Maximum number of open issues to inspect.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--apply",
|
||||||
|
action="store_true",
|
||||||
|
help="Actually close duplicate issues. Defaults to dry-run.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def run_gh_command(args: list[str]) -> str:
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(
|
||||||
|
args,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise RuntimeError("GitHub CLI `gh` is not installed or not in PATH.") from exc
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
stderr = exc.stderr.strip()
|
||||||
|
stdout = exc.stdout.strip()
|
||||||
|
details = stderr or stdout or str(exc)
|
||||||
|
raise RuntimeError(f"`{' '.join(args)}` failed: {details}") from exc
|
||||||
|
return completed.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def load_open_issues(repo: str, label: str, limit: int) -> list[Issue]:
|
||||||
|
output = run_gh_command(
|
||||||
|
[
|
||||||
|
"gh",
|
||||||
|
"issue",
|
||||||
|
"list",
|
||||||
|
"--repo",
|
||||||
|
repo,
|
||||||
|
"--label",
|
||||||
|
label,
|
||||||
|
"--state",
|
||||||
|
"open",
|
||||||
|
"--limit",
|
||||||
|
str(limit),
|
||||||
|
"--json",
|
||||||
|
"number,title,createdAt,url",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
items = json.loads(output)
|
||||||
|
return [
|
||||||
|
Issue(
|
||||||
|
number=item["number"],
|
||||||
|
title=item["title"],
|
||||||
|
created_at=datetime.fromisoformat(item["createdAt"].replace("Z", "+00:00")),
|
||||||
|
url=item["url"],
|
||||||
|
)
|
||||||
|
for item in items
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_title(title: str) -> str:
|
||||||
|
return " ".join(title.split()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def find_duplicates(
|
||||||
|
issues: list[Issue],
|
||||||
|
) -> list[tuple[Issue, list[Issue]]]:
|
||||||
|
grouped: dict[str, list[Issue]] = defaultdict(list)
|
||||||
|
for issue in issues:
|
||||||
|
grouped[normalize_title(issue.title)].append(issue)
|
||||||
|
|
||||||
|
duplicate_groups: list[tuple[Issue, list[Issue]]] = []
|
||||||
|
for group in grouped.values():
|
||||||
|
if len(group) < 2:
|
||||||
|
continue
|
||||||
|
ordered = sorted(
|
||||||
|
group,
|
||||||
|
key=lambda issue: (issue.created_at, issue.number),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
keep = ordered[0]
|
||||||
|
close_candidates = ordered[1:]
|
||||||
|
duplicate_groups.append((keep, close_candidates))
|
||||||
|
|
||||||
|
duplicate_groups.sort(
|
||||||
|
key=lambda item: (item[0].created_at, item[0].number),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return duplicate_groups
|
||||||
|
|
||||||
|
|
||||||
|
def print_plan(duplicate_groups: list[tuple[Issue, list[Issue]]], apply: bool) -> None:
|
||||||
|
action = "Will close" if apply else "Would close"
|
||||||
|
if not duplicate_groups:
|
||||||
|
print("No duplicate open issues found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
total_to_close = sum(len(close_list) for _, close_list in duplicate_groups)
|
||||||
|
print(f"Found {len(duplicate_groups)} duplicate title groups.")
|
||||||
|
print(
|
||||||
|
f"{action} {total_to_close} issues and keep {len(duplicate_groups)} latest issues."
|
||||||
|
)
|
||||||
|
|
||||||
|
for keep, close_list in duplicate_groups:
|
||||||
|
print()
|
||||||
|
print(f'Keep #{keep.number} [{keep.created_at.isoformat()}] "{keep.title}"')
|
||||||
|
print(f" {keep.url}")
|
||||||
|
for issue in close_list:
|
||||||
|
print(
|
||||||
|
f'Close #{issue.number} [{issue.created_at.isoformat()}] "{issue.title}"'
|
||||||
|
)
|
||||||
|
print(f" {issue.url}")
|
||||||
|
|
||||||
|
|
||||||
|
def close_duplicates(
|
||||||
|
repo: str, duplicate_groups: list[tuple[Issue, list[Issue]]]
|
||||||
|
) -> None:
|
||||||
|
for keep, close_list in duplicate_groups:
|
||||||
|
reason = (
|
||||||
|
f"Closing as duplicate of #{keep.number}. "
|
||||||
|
"Keeping the latest open issue with this title."
|
||||||
|
)
|
||||||
|
for issue in close_list:
|
||||||
|
print(f"Closing #{issue.number} as duplicate of #{keep.number}...")
|
||||||
|
run_gh_command(
|
||||||
|
[
|
||||||
|
"gh",
|
||||||
|
"issue",
|
||||||
|
"close",
|
||||||
|
str(issue.number),
|
||||||
|
"--repo",
|
||||||
|
repo,
|
||||||
|
"--comment",
|
||||||
|
reason,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
try:
|
||||||
|
issues = load_open_issues(args.repo, args.label, args.limit)
|
||||||
|
duplicate_groups = find_duplicates(issues)
|
||||||
|
print_plan(duplicate_groups, apply=args.apply)
|
||||||
|
if args.apply and duplicate_groups:
|
||||||
|
print()
|
||||||
|
close_duplicates(args.repo, duplicate_groups)
|
||||||
|
print("Done.")
|
||||||
|
elif not args.apply:
|
||||||
|
print()
|
||||||
|
print("Dry-run only. Re-run with `--apply` to close the duplicates.")
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(str(exc), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user