From 8e90e1b114da3fcb0b44cd269435985226df30a0 Mon Sep 17 00:00:00 2001 From: Gloire Rubambiza Date: Mon, 17 Aug 2026 14:52:51 -0400 Subject: [PATCH 1/5] feat: Add --repos flag to scope report.py to explicit repos Assisted-By: Claude Code (Anthropic AI) Signed-off-by: Gloire Rubambiza --- skills/github-weekly-report/scripts/report.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index b005a5b..6cae043 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -29,6 +29,21 @@ def run_gh(args): def get_repos(org): return run_gh(['repo', 'list', org, '--limit', '100', '--json', 'name']) + +def normalize_repo_args(repo_args): + """Turn owner-qualified --repos values into the bare-name dicts get_repos + returns. 'rossoctl/operator' -> {'name': 'operator'}. A bare 'operator' + (no slash) is accepted as-is. Order and duplicates are preserved as given. + """ + names = [] + for item in repo_args: + item = item.strip() + if not item: + continue + _, _, name = item.rpartition('/') + names.append({'name': name or item}) + 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): @@ -209,9 +224,9 @@ def render_active_epics_section(data): lines.append("") return lines -def generate_report(org, since, until, enhanced=False): +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})*", ""] - repos = get_repos(org) + repos = repos if repos is not None else get_repos(org) repos_data = [] for repo in repos: name = repo['name'] @@ -465,11 +480,14 @@ 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='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') - report, repos_data, epic_data = generate_report(args.org, since, until, args.enhanced) + repos = normalize_repo_args(args.repos) 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: From ba430b89b5ad07ea0c143c2c18a678f3901e350e Mon Sep 17 00:00:00 2001 From: Gloire Rubambiza Date: Mon, 17 Aug 2026 15:14:14 -0400 Subject: [PATCH 2/5] feat: Scope epic-tracker to --repos and thread it from report.py Assisted-By: Claude Code (Anthropic AI) Signed-off-by: Gloire Rubambiza --- skills/github-weekly-report/scripts/epic-tracker.py | 10 ++++++++-- skills/github-weekly-report/scripts/report.py | 12 ++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) 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 6cae043..682bee6 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -155,7 +155,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 @@ -168,10 +168,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) @@ -313,7 +313,7 @@ def generate_report(org, since, until, enhanced=False, repos=None): 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("") From 7702afcfc461aeb79680b3ed591a3b8df8553c58 Mon Sep 17 00:00:00 2001 From: Gloire Rubambiza Date: Mon, 17 Aug 2026 15:33:23 -0400 Subject: [PATCH 3/5] docs: Document --repos flag in weekly-report SKILL.md Assisted-By: Claude Code (Anthropic AI) Signed-off-by: Gloire Rubambiza --- skills/github-weekly-report/SKILL.md | 5 +++++ 1 file changed, 5 insertions(+) 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 From 6ca3d26dcb64fa53c4b437905fc42f4e03df243f Mon Sep 17 00:00:00 2001 From: Gloire Rubambiza Date: Mon, 17 Aug 2026 20:54:09 -0400 Subject: [PATCH 4/5] feat: Add skill breadcrumb to report header Link the report header back to the github-weekly-report skill so a reader can find and improve the generator, matching the breadcrumb pattern requested in rossoctl/automation#41. Assisted-By: Claude Code (Anthropic AI) Signed-off-by: Gloire Rubambiza --- skills/github-weekly-report/scripts/report.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index 682bee6..c43ce18 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -225,7 +225,14 @@ def render_active_epics_section(data): return lines 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})*", ""] + 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: From f4ae90e900007859316b68a2448dff9c4ca8a615 Mon Sep 17 00:00:00 2001 From: Gloire Rubambiza Date: Thu, 20 Aug 2026 12:03:39 -0400 Subject: [PATCH 5/5] fix: Reject --repos entries outside --org normalize_repo_args parsed OWNER/REPO and discarded the owner, while every downstream fetch rebuilds the slug as f'{org}/{name}'. An entry like 'otherorg/operator' was therefore queried as '{--org}/operator' and silently reported against the wrong owner; a malformed 'a/b/c' was also accepted. Scope the flag to a single owner: accept a bare name or an owner-qualified name whose owner matches --org, and exit with a clear error otherwise (or on a malformed entry). Update the metavar to REPO and document the within-org contract. The automation wrapper (rossoctl/automation#59) emits '$ORG/name' (owner always == $ORG), so it stays compatible with this contract. Cross-owner repo sets are out of scope for this flag; broadening to arbitrary owners is a separate design discussion. Addresses esnible's must-fix review on #32. Assisted-By: Claude Code (Anthropic AI) Signed-off-by: Gloire Rubambiza --- skills/github-weekly-report/scripts/report.py | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/skills/github-weekly-report/scripts/report.py b/skills/github-weekly-report/scripts/report.py index c43ce18..4cf00b0 100644 --- a/skills/github-weekly-report/scripts/report.py +++ b/skills/github-weekly-report/scripts/report.py @@ -30,18 +30,35 @@ def run_gh(args): def get_repos(org): return run_gh(['repo', 'list', org, '--limit', '100', '--json', 'name']) -def normalize_repo_args(repo_args): - """Turn owner-qualified --repos values into the bare-name dicts get_repos - returns. 'rossoctl/operator' -> {'name': 'operator'}. A bare 'operator' - (no slash) is accepted as-is. Order and duplicates are preserved as given. +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 - _, _, name = item.rpartition('/') - names.append({'name': name or item}) + 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): @@ -487,13 +504,14 @@ 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='OWNER/REPO', - help='Explicit owner-qualified repo list; when set, skips org-wide discovery') + 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') - repos = normalize_repo_args(args.repos) if args.repos else None + 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: