diff --git a/skills/github-weekly-report/SKILL.md b/skills/github-weekly-report/SKILL.md index 0a6e42a..c06e3f6 100644 --- a/skills/github-weekly-report/SKILL.md +++ b/skills/github-weekly-report/SKILL.md @@ -27,6 +27,9 @@ python3 {baseDir}/scripts/report.py --org --output report.md --json-output # Report on specific date range python3 {baseDir}/scripts/report.py --org --since 2026-03-23 --until 2026-03-30 --output report.md --json-output report-data.json +# Report on an explicit repo set (owner-qualified); skips org-wide discovery +python3 {baseDir}/scripts/report.py --org --repos /repo-a /repo-b --output report.md --json-output report-data.json + # Run epic tracker standalone (for debugging) python3 {baseDir}/scripts/epic-tracker.py --org --since 2026-03-23 --until 2026-03-30 @@ -34,6 +37,8 @@ python3 {baseDir}/scripts/epic-tracker.py --org --since 2026-03-23 --until gh issue create -R / --title "Weekly Report $(date +%Y-%m-%d)" --body-file report.md ``` +When `--repos` is omitted, the report discovers all repos in the org (the unchanged default). Automation deployments pass the curated core-repo allowlist via the `weekly-report.sh` wrapper, which resolves the list and calls `report.py --repos` for you. + ## What the Report Includes 1. **Org-Wide Summary** — table with merged PRs, open PRs, open issues, new issues, CI pass rate, and status per repo diff --git a/skills/github-weekly-report/scripts/epic-tracker.py b/skills/github-weekly-report/scripts/epic-tracker.py index 2c79051..ddba63f 100644 --- a/skills/github-weekly-report/scripts/epic-tracker.py +++ b/skills/github-weekly-report/scripts/epic-tracker.py @@ -258,13 +258,19 @@ def main(): p.add_argument('--until', help='End of reporting period (YYYY-MM-DD)') p.add_argument('--max-epics', type=int, default=10, help='Maximum epics to include') p.add_argument('--skip-projects', action='store_true', help='Skip GitHub Projects v2 query') + p.add_argument('--repos', nargs='+', metavar='OWNER/REPO', + help='Explicit owner-qualified repo list; when set, skips org-wide discovery') args = p.parse_args() since = args.since or (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') until = args.until or datetime.now().strftime('%Y-%m-%d') - print(f"Fetching repos for {args.org}...", file=sys.stderr) - repos = get_org_repos(args.org) + if args.repos: + # Owner-qualified -> bare names; get_epics builds 'org/repo' from --org. + repos = [item.rpartition('/')[2] or item for item in args.repos] + else: + print(f"Fetching repos for {args.org}...", file=sys.stderr) + repos = get_org_repos(args.org) if not repos: print("Error: no repos found", file=sys.stderr) json.dump({'epics': [], 'fallback_mode': True, 'period': {'since': since, 'until': until}}, sys.stdout, indent=2) diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index b005a5b..4cf00b0 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -29,6 +29,38 @@ def run_gh(args): def get_repos(org): return run_gh(['repo', 'list', org, '--limit', '100', '--json', 'name']) + +def normalize_repo_args(repo_args, org): + """Turn --repos values into the bare-name dicts get_repos returns, scoped to + a single owner. Accepts a bare name ('operator') or an owner-qualified name + whose owner matches --org ('rossoctl/operator'); both yield {'name': 'operator'}. + + Every downstream fetch rebuilds the slug as f'{org}/{name}', so an entry whose + owner differs from --org would be silently reported against the wrong owner. + To keep the report header and the queried data consistent, reject any entry + that is owner-qualified with a different owner, or that is malformed (empty, + or more than one '/'). Order and duplicates are preserved as given. + """ + names = [] + for item in repo_args: + item = item.strip() + if not item: + continue + parts = item.split('/') + if len(parts) == 1: + name = parts[0] + elif len(parts) == 2 and parts[0] and parts[1]: + owner, name = parts + if owner != org: + sys.exit( + f"--repos entry '{item}' is not in --org '{org}'. " + f"Entries must be a bare repo name or '{org}/'." + ) + else: + sys.exit(f"--repos entry '{item}' is malformed; expected '' or '{org}/'.") + names.append({'name': name}) + return names + def get_merged_prs(org, repo, since, until): return run_gh(['pr', 'list', '-R', f'{org}/{repo}', '--search', f'merged:{since}..{until}', '--state', 'merged', '--limit', '500', '--json', 'number,title,author,mergedAt']) def get_open_prs(org, repo): @@ -140,7 +172,7 @@ def generate_action_items(repos_data): lines.append("") return lines -def run_epic_tracker(org, since, until): +def run_epic_tracker(org, since, until, repos=None): """Run epic-tracker.py once and return its parsed JSON. Returns a dict on success, or a dict with an 'error' key describing why @@ -153,10 +185,10 @@ def run_epic_tracker(org, since, until): return {'error': 'Epic tracker not available.'} try: - result = subprocess.run( - [sys.executable, tracker, '--org', org, '--since', since, '--until', until], - capture_output=True, text=True, timeout=120 - ) + cmd = [sys.executable, tracker, '--org', org, '--since', since, '--until', until] + if repos: + cmd += ['--repos', *[r['name'] for r in repos]] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode != 0: if result.stderr: print(f"epic-tracker stderr: {result.stderr}", file=sys.stderr) @@ -209,9 +241,16 @@ def render_active_epics_section(data): lines.append("") return lines -def generate_report(org, since, until, enhanced=False): - lines = [f"# Org Weekly Report: {since} -- {until}", "", f"*Generated for [{org}](https://github.com/{org})*", ""] - repos = get_repos(org) +def generate_report(org, since, until, enhanced=False, repos=None): + lines = [ + f"# Org Weekly Report: {since} -- {until}", + "", + f"*Generated for [{org}](https://github.com/{org}) by the " + "[github-weekly-report](https://github.com/rossoctl/agent-skills/tree/main/skills/github-weekly-report) " + "skill.*", + "", + ] + repos = repos if repos is not None else get_repos(org) repos_data = [] for repo in repos: name = repo['name'] @@ -298,7 +337,7 @@ def generate_report(org, since, until, enhanced=False): lines.append("") # Active Epics — run the tracker once and reuse for both markdown and JSON - epic_data = run_epic_tracker(org, since, until) + epic_data = run_epic_tracker(org, since, until, repos=repos) lines += render_active_epics_section(epic_data) lines.append("") @@ -465,11 +504,15 @@ def main(): p.add_argument('--output') p.add_argument('--json-output', metavar='PATH', help='Write structured JSON data for AI synthesis') p.add_argument('--enhanced', action='store_true', help='Include additional metrics (reserved for future use)') + p.add_argument('--repos', nargs='+', metavar='REPO', + help="Explicit repo list scoped to --org (bare '' or " + "'/'); when set, skips org-wide discovery") args = p.parse_args() since = args.since or (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') until = args.until or datetime.now().strftime('%Y-%m-%d') - report, repos_data, epic_data = generate_report(args.org, since, until, args.enhanced) + repos = normalize_repo_args(args.repos, args.org) if args.repos else None + report, repos_data, epic_data = generate_report(args.org, since, until, args.enhanced, repos=repos) if args.output: with open(args.output, 'w') as f: