From 8979e346fa5f769d9b77f6b9451d000b6ef080d2 Mon Sep 17 00:00:00 2001 From: Gloire Rubambiza Date: Mon, 17 Aug 2026 15:46:00 -0400 Subject: [PATCH 1/2] feat: Add weekly-report.sh wrapper scoped to core repos Assisted-By: Claude Code (Anthropic AI) Signed-off-by: Gloire Rubambiza --- scripts/weekly-report.sh | 94 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100755 scripts/weekly-report.sh diff --git a/scripts/weekly-report.sh b/scripts/weekly-report.sh new file mode 100755 index 0000000..8d84130 --- /dev/null +++ b/scripts/weekly-report.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# Weekly Report (core-scoped) +# Resolves the org and the curated core-repo allowlist via the shared library, +# then invokes the Python report generator scoped to exactly those repos. +# +# Usage: +# bash weekly-report.sh --help +# bash weekly-report.sh --output /tmp/report.md --json-output /tmp/report-data.json +# bash weekly-report.sh --org rossoctl --since 2026-08-10 --until 2026-08-17 +# ============================================================================= + +# --- Load shared library --- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/program-lib.sh" + +# --- CLI args --- +SINCE="" +UNTIL="" +OUTPUT="" +JSON_OUTPUT="" +SHOW_HELP=false + +while [[ $# -gt 0 ]]; do + case $1 in + --since) SINCE="$2"; shift 2 ;; + --until) UNTIL="$2"; shift 2 ;; + --output) OUTPUT="$2"; shift 2 ;; + --json-output) JSON_OUTPUT="$2"; shift 2 ;; + --profile) PROFILE_FLAG="$2"; shift 2 ;; + --org) ORG_FLAG="$2"; shift 2 ;; + --help|-h) SHOW_HELP=true; shift ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [ "$SHOW_HELP" = true ]; then + cat << 'USAGE' +weekly-report -- Generate the weekly org report scoped to the core repos + +USAGE: + weekly-report.sh [OPTIONS] + +OPTIONS: + --since DATE Start of reporting window (YYYY-MM-DD; default: 7 days ago) + --until DATE End of reporting window (YYYY-MM-DD; default: today) + --output FILE Write the Markdown report to FILE (default: stdout) + --json-output FILE Write structured JSON for AI synthesis to FILE + --profile NAME Org profile to load (config/org..env; default org.env) + --org NAME GitHub org (default: from profile, config/org.env) + --help, -h Show this help + +ENVIRONMENT: + REPORT_PY Path to report.py (default: the deployed report generator) + +PREREQUISITES: + python3, gh (authenticated). The core-repo list comes from config/core-repos.txt + via the shared library (get_core_repos). +USAGE + exit 0 +fi + +# Resolve org identity (sets ORG, honoring --org/--profile/env/profile precedence). +load_org_profile + +# The curated allowlist, owner-qualified (e.g. rossoctl/operator), one per line. +repos="$(get_core_repos)" +if [ -z "$repos" ]; then + echo "Error: get_core_repos returned no repos" >&2 + exit 1 +fi + +# Locate the generator. Default points at the deployed report generator; override +# with REPORT_PY in dev. +REPORT_PY="${REPORT_PY:-$HOME/workspaces/shared/skills/github-report-generator/scripts/report.py}" +if [ ! -f "$REPORT_PY" ]; then + echo "Error: report generator not found at: $REPORT_PY" >&2 + echo "Set REPORT_PY to the path of report.py." >&2 + exit 1 +fi + +# Build args. $repos is intentionally unquoted so each line becomes a separate +# --repos value; core repo names never contain whitespace. +# shellcheck disable=SC2086 +set -- --org "$ORG" --repos $repos +[ -n "$SINCE" ] && set -- "$@" --since "$SINCE" +[ -n "$UNTIL" ] && set -- "$@" --until "$UNTIL" +[ -n "$OUTPUT" ] && set -- "$@" --output "$OUTPUT" +[ -n "$JSON_OUTPUT" ] && set -- "$@" --json-output "$JSON_OUTPUT" + +exec python3 "$REPORT_PY" "$@" From 2de723b86accdd59df9a53d93d0edfb8109ee10d Mon Sep 17 00:00:00 2001 From: Gloire Rubambiza Date: Mon, 17 Aug 2026 15:58:13 -0400 Subject: [PATCH 2/2] test: Add hermetic test for weekly-report.sh wrapper Assisted-By: Claude Code (Anthropic AI) Signed-off-by: Gloire Rubambiza --- .github/workflows/tests.yml | 2 +- tests/test-weekly-report.sh | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100755 tests/test-weekly-report.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcc6568..b1f9748 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: for t in tests/test-lib-inventory.sh tests/test-lib-modules.sh \ tests/test-core-repos.sh tests/test-org-profile.sh \ tests/test-extract-broken-links.sh tests/test-parse-diff-map.sh \ - tests/test-pr-review-impact.sh; do + tests/test-pr-review-impact.sh tests/test-weekly-report.sh; do echo "== $t ==" bash "$t" done diff --git a/tests/test-weekly-report.sh b/tests/test-weekly-report.sh new file mode 100755 index 0000000..e2bf2a0 --- /dev/null +++ b/tests/test-weekly-report.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Verifies scripts/weekly-report.sh builds the generator invocation from the +# core-repo allowlist. Hermetic: a fixture allowlist via $CORE_REPOS_FILE and a +# stub report.py via $REPORT_PY, so no gh / network / real config is touched. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WRAPPER="$SCRIPT_DIR/../scripts/weekly-report.sh" + +TEST_TMPDIR=$(mktemp -d) +trap 'rm -rf "$TEST_TMPDIR"' EXIT + +fail=0 + +# Fixture allowlist (bare names; owner is derived from ORG). +cat > "$TEST_TMPDIR/repos.txt" <<'EOF' +# comment +alpha +beta +EOF + +# Stub generator: echo the args it was called with. +cat > "$TEST_TMPDIR/report.py" <<'PY' +import sys +print(" ".join(sys.argv[1:])) +PY + +got=$(ORG=rossoctl \ + CORE_REPOS_FILE="$TEST_TMPDIR/repos.txt" \ + REPORT_PY="$TEST_TMPDIR/report.py" \ + bash "$WRAPPER" --output /tmp/ignored.md) +want="--org rossoctl --repos rossoctl/alpha rossoctl/beta --output /tmp/ignored.md" +[ "$got" = "$want" ] || { echo "FAIL wrapper args: got [$got] want [$want]"; fail=1; } + +# --since / --until pass through. +got2=$(ORG=rossoctl \ + CORE_REPOS_FILE="$TEST_TMPDIR/repos.txt" \ + REPORT_PY="$TEST_TMPDIR/report.py" \ + bash "$WRAPPER" --since 2026-08-10 --until 2026-08-17) +want2="--org rossoctl --repos rossoctl/alpha rossoctl/beta --since 2026-08-10 --until 2026-08-17" +[ "$got2" = "$want2" ] || { echo "FAIL wrapper window args: got [$got2] want [$want2]"; fail=1; } + +# Missing generator fails loud. +if ORG=rossoctl CORE_REPOS_FILE="$TEST_TMPDIR/repos.txt" \ + REPORT_PY="$TEST_TMPDIR/does-not-exist.py" \ + bash "$WRAPPER" >/dev/null 2>&1; then + echo "FAIL wrapper should error on missing REPORT_PY"; fail=1 +fi + +if [ "$fail" -eq 0 ]; then echo "PASS test-weekly-report.sh"; fi +exit "$fail"