diff --git a/.gitignore b/.gitignore index 6c7f643..5e80c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ reports/ *.log + +# Local copies of host cron/job state (contains deployment internals; never commit) +.host-backup/ diff --git a/README.md b/README.md index 00a01ae..d1df87c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,28 @@ reports/ (gitignored) Runtime data stays on remote host only | Dependency Bump | `scripts/dep-bump-scanner.sh` | `scripts/dep-bump-fixer.sh` | Tracks and triages Dependabot PRs across repos (comments, closes stale, audits coverage) | | Health Dashboard | `scripts/automation-health-dashboard.sh` | — | Aggregates program run health into a dashboard | -Shared helpers live in `scripts/program-lib.sh`. Repo coverage comes from the `config/core-repos.txt` allowlist via `get_core_repos()` (one `owner/name` per line; `#` comments and blank lines allowed) -- edit that file to change which repos are scanned. +Shared helpers live in `scripts/program-lib.sh`. Repo coverage comes from the `config/core-repos.txt` allowlist via `get_core_repos()` (one **bare repo name** per line -- the owner is prepended from the active org; `#` comments and blank lines allowed) -- edit that file to change which repos are scanned. + +## Org profile + +Org identity (which GitHub org the programs target) lives in one committed profile file, `config/org.env`. It assigns only `PROFILE_`-prefixed keys: + +```bash +PROFILE_ORG=rossoctl # the org (required) +PROFILE_FORK_OWNER=clawgenti # fork account for cross-fork PRs +PROFILE_MAIN_REPO=rossoctl/rossoctl +PROFILE_REPOS_DIR=/home/claw/rossoctl +PROFILE_REMAP="kagenti:rossoctl kagenti-extensions:cortex" # transitional +``` + +Each program calls `load_org_profile()` (in `program-lib.sh`), which resolves the identity by precedence **`--flag` > env var > profile > built-in default**: + +- `--org NAME` / `ORG` -- the org (fails loud if it cannot be resolved). +- `--fork-owner NAME` / `FORK_OWNER` -- fork account (default `clawgenti`). +- `--repos-dir DIR` / `REPOS_DIR` -- local clone root (default `$HOME/$ORG`). +- `--main-repo-dir DIR` / `MAIN_REPO_DIR` -- the main-repo clone for dashboard/report git ops. + +`PROFILE_REMAP` is profile-only (transitional; maps pre-rename clone-dir names to canonical names). To target a **different org**, copy `config/org.env` to `config/org..env`, edit the `PROFILE_*` values, and run any program with `--profile ` (or `ORG_PROFILE=`). No per-script edits are needed. ## Deploy @@ -35,10 +56,10 @@ scp scripts/.sh kagenti-bot:~/workspaces/clawgenti/scripts/ No gateway restart needed -- scripts are read from disk on each cron trigger. -Scanners/fixers that use the allowlist also require `config/core-repos.txt` to be present on the host alongside the scripts: +All programs resolve their org identity from `config/org.env`, and allowlist-driven scanners/fixers also read `config/core-repos.txt`. Both must be present on the host alongside the scripts: ```bash -scp config/core-repos.txt kagenti-bot:~/workspaces/clawgenti/config/ +scp config/org.env config/core-repos.txt kagenti-bot:~/workspaces/clawgenti/config/ ``` ## Runtime diff --git a/automation-health/README.md b/automation-health/README.md new file mode 100644 index 0000000..764f221 --- /dev/null +++ b/automation-health/README.md @@ -0,0 +1,20 @@ +# automation-health + +Machine-generated reports from the automation programs. These files are +**overwritten in place on every run** by their generating scripts — do not edit +them by hand, and do not add dated copies. + +| File | Generated by | Cadence | +|------|--------------|---------| +| `link-health.md` | `scripts/link-health-scanner.sh` | per scan run | +| `automation-health.md` | `scripts/automation-health-dashboard.sh` | per dashboard run | + +## Why a single overwritten file (not dated snapshots) + +Trends are reconstructed by replaying the file's git commit history (parent by +parent), so each commit already captures a point-in-time snapshot. Storing dated +copies would duplicate what git already records — the files-vs-diffs-on-Git +anti-pattern. A future trend tool should read `git log` for these paths rather +than expect a directory of dated files. + +See rossoctl/automation#44 for the decision. diff --git a/config/org.env b/config/org.env index 488f778..cf841ba 100644 --- a/config/org.env +++ b/config/org.env @@ -13,6 +13,12 @@ PROFILE_FORK_OWNER=clawgenti PROFILE_MAIN_REPO=rossoctl/rossoctl PROFILE_REPOS_DIR=${HOME}/rossoctl +# Repo where this suite (scripts, skills, standing orders) is version-controlled. +# Report PRs link back to the invoking program's standing order here for +# auditability. Defaults to "$ORG/automation" if unset; override if a fork keeps +# its automation elsewhere. +# PROFILE_SOURCE_REPO=rossoctl/automation + # TRANSITIONAL: maps pre-rename clone-dir basenames to canonical repo names. # Self-retires once host clone dirs are renamed (rossoctl/automation#37): # delete this line and the remap becomes pure identity. diff --git a/docs/running-without-openclaw.md b/docs/running-without-openclaw.md index d354942..2e1ccd3 100644 --- a/docs/running-without-openclaw.md +++ b/docs/running-without-openclaw.md @@ -160,10 +160,33 @@ None of these are required for the scripts to work. They're convenience features ## Adapting for a Different Org -To run against a different GitHub org: +Org identity is no longer edited into each script. It lives in a committed +profile file, `config/org.env`, resolved by `load_org_profile()` with +precedence `--flag > env > profile > default`. To run against a different org: -1. Clone that org's repos into `$REPOS_DIR` -2. Edit `FORK_OWNER` in the fixer (or set it as an env var) -3. Edit `ORG="rossoctl"` in the fixer to your org name -4. Update DCO identity in the fixer (`GIT_AUTHOR_NAME` / `GIT_AUTHOR_EMAIL`) -5. Run the scanner -- it will detect broken links and create issues in the target repos +1. Copy the default profile and edit the `PROFILE_*` values for your org: + + ```bash + cp config/org.env config/org.myorg.env + # edit config/org.myorg.env: + # PROFILE_ORG=myorg + # PROFILE_FORK_OWNER=myfork + # PROFILE_MAIN_REPO=myorg/myorg + # PROFILE_REPOS_DIR=$HOME/myorg + # PROFILE_REMAP="" # only for transitional dir renames + ``` + +2. Clone that org's repos into the profile's `PROFILE_REPOS_DIR` (or override + at run time with `--repos-dir` / `$REPOS_DIR`). + +3. Run any program with `--profile myorg` (or `ORG_PROFILE=myorg`): + + ```bash + bash scripts/link-health-scanner.sh --profile myorg --dry-run + ``` + + Individual facts can still be overridden per run without touching the + profile, e.g. `--org myorg --fork-owner myfork --repos-dir ~/myorg`. + +4. Update the DCO identity in the fixer (`GIT_AUTHOR_NAME` / `GIT_AUTHOR_EMAIL`) + if fix PRs should be attributed to a different maintainer. diff --git a/scripts/automation-health-dashboard.sh b/scripts/automation-health-dashboard.sh index 1f89f7e..2ff9090 100644 --- a/scripts/automation-health-dashboard.sh +++ b/scripts/automation-health-dashboard.sh @@ -2,7 +2,7 @@ set -euo pipefail # ============================================================================= -# Automation Health Dashboard Generator — kagenti org +# Automation Health Dashboard Generator # Combines link-health and dep-bump program metrics into a single executive- # facing markdown dashboard. Pushes to a standing fork-based PR. # @@ -13,23 +13,24 @@ set -euo pipefail # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/program-lib.sh" # --- CLI args --- DRY_RUN=true -ORG="kagenti" VERBOSE=false SHOW_HELP=false -FORK_OWNER="${FORK_OWNER:-clawgenti}" -KAGENTI_DIR="${KAGENTI_DIR:-}" +MAIN_REPO_DIR="${MAIN_REPO_DIR:-}" while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true; shift ;; --live) DRY_RUN=false; shift ;; --reports-dir) REPORTS_DIR="$2"; shift 2 ;; - --kagenti-dir) KAGENTI_DIR="$2"; shift 2 ;; - --org) ORG="$2"; shift 2 ;; - --fork-owner) FORK_OWNER="$2"; shift 2 ;; + --main-repo-dir) MAIN_REPO_DIR="$2"; shift 2 ;; + --profile) PROFILE_FLAG="$2"; shift 2 ;; + --org) ORG_FLAG="$2"; shift 2 ;; + --fork-owner) FORK_OWNER_FLAG="$2"; shift 2 ;; --verbose) VERBOSE=true; shift ;; --help|-h) SHOW_HELP=true; shift ;; *) echo "Unknown option: $1"; exit 1 ;; @@ -49,21 +50,41 @@ Usage: Options: --dry-run Generate and preview dashboard (default) --live Commit and push to fork, create/update PR - --reports-dir DIR Base reports directory (default: $REPORTS_DIR or ./reports) - --kagenti-dir DIR Path to kagenti repo clone (default: $KAGENTI_DIR) - --org NAME GitHub org (default: kagenti) - --fork-owner NAME Fork owner for PR workflow (default: clawgenti) - --verbose Print diagnostic output - --help, -h Show this help + --reports-dir DIR Base reports directory (default: $REPORTS_DIR or ./reports) + --main-repo-dir DIR Path to the report-target repo clone, overriding the + REPOS_DIR-derived default (default: $MAIN_REPO_DIR) + --profile NAME Org profile to load (config/org..env; default org.env) + --org NAME GitHub org (default: from profile, config/org.env) + --fork-owner NAME Fork owner for PR workflow (default: from profile) + --verbose Print diagnostic output + --help, -h Show this help Environment: - REPORTS_DIR Base directory containing link-scan/ and dep-bump/ subdirs - KAGENTI_DIR Path to the org's main repo clone (for live mode git operations) - FORK_OWNER Fork owner for cross-fork PRs + REPORTS_DIR Base directory containing link-scan/ and dep-bump/ subdirs + MAIN_REPO_DIR Path to the report-target repo clone (live mode git ops); + overrides the REPOS_DIR-derived default + FORK_OWNER Fork owner for cross-fork PRs HELP exit 0 fi +# Resolve org identity (--flag > env > profile > default). Sets ORG, FORK_OWNER, +# MAIN_REPO, REPOS_DIR, REMAP. +load_org_profile + +# Report-PR destination. The org main repo's docs/ folder feeds the docs site +# (rossoctl.dev) and cannot host machine-generated reports, so the standing +# dashboard PR lands under automation-health/ in the automation repo. A single +# file, overwritten in place each run: trend tooling reconstructs history by +# replaying git commit parents, so we store state (not dated snapshots) and +# avoid the files-vs-diffs-on-Git anti-pattern (rossoctl/automation#44). +REPORT_TARGET_REPO="$ORG/automation" +REPORT_TARGET_NAME="${REPORT_TARGET_REPO##*/}" +REPORT_TARGET_PATH="automation-health/automation-health.md" +# Clone dir for the report target: honor an explicit --main-repo-dir/MAIN_REPO_DIR +# override, else derive from REPOS_DIR. +REPORT_TARGET_DIR="${MAIN_REPO_DIR:-$REPOS_DIR/$REPORT_TARGET_NAME}" + # --- Validate inputs --- if [ -z "${REPORTS_DIR:-}" ]; then if [ -d "./reports" ]; then @@ -418,7 +439,7 @@ $COVERAGE_TABLE $CRON_TABLE --- -*Generated by Kagenti Automation Health Dashboard. Do not edit manually.* +*Generated by Rossoctl Automation Health Dashboard. Do not edit manually.* DASHBOARD_EOF echo "Dashboard generated ($TMPDIR/automation-health.md)" @@ -433,29 +454,26 @@ if [ "$DRY_RUN" = true ]; then echo "---" cat "$TMPDIR/automation-health.md" echo "---" - echo "[DRY RUN] Would push docs/automation-health.md to fork and create/update PR" + echo "[DRY RUN] Would push $REPORT_TARGET_PATH to fork and create/update PR against $REPORT_TARGET_REPO" else - if [ -z "$KAGENTI_DIR" ]; then - echo "ERROR: KAGENTI_DIR is not set (required for live mode)." - echo "Export it to the path of the kagenti/kagenti repo clone:" - echo " export KAGENTI_DIR=~/kagenti/kagenti" - exit 1 - fi - - if [ ! -d "$KAGENTI_DIR/.git" ]; then - echo "ERROR: $KAGENTI_DIR does not appear to be a git repository." + if [ ! -d "$REPORT_TARGET_DIR/.git" ]; then + echo "ERROR: $REPORT_TARGET_DIR does not appear to be a git repository." + echo "Export MAIN_REPO_DIR or set REPOS_DIR so $REPORT_TARGET_REPO can be found:" + echo " export MAIN_REPO_DIR=$REPOS_DIR/$REPORT_TARGET_NAME" exit 1 fi FORK_REMOTE="$FORK_OWNER" DASHBOARD_BRANCH="automation/health-dashboard" - cd "$KAGENTI_DIR" + cd "$REPORT_TARGET_DIR" - # Ensure fork remote exists - if ! git remote get-url "$FORK_REMOTE" &>/dev/null; then - git remote add "$FORK_REMOTE" "https://github.com/$FORK_OWNER/$ORG.git" - fi + # Ensure the fork remote exists AND points at the current target. set-url + # corrects a stale remote (e.g. one left by a prior deployment pointing at the + # old report repo); the || add branch handles the not-yet-registered case. + fork_url="https://github.com/$FORK_OWNER/${REPORT_TARGET_NAME}.git" + git remote set-url "$FORK_REMOTE" "$fork_url" 2>/dev/null \ + || git remote add "$FORK_REMOTE" "$fork_url" # Fetch fork's branch if it exists, otherwise create from main if git fetch "$FORK_REMOTE" "$DASHBOARD_BRANCH" 2>/dev/null; then @@ -466,19 +484,19 @@ else || git checkout -B "$DASHBOARD_BRANCH" fi - mkdir -p docs - cp "$TMPDIR/automation-health.md" docs/automation-health.md - git add docs/automation-health.md + mkdir -p "$(dirname "$REPORT_TARGET_PATH")" + cp "$TMPDIR/automation-health.md" "$REPORT_TARGET_PATH" + git add "$REPORT_TARGET_PATH" git commit -s -m "docs: Update automation health dashboard ($SCAN_TIME_ET)" 2>/dev/null || echo "No changes to commit" git push "$FORK_REMOTE" "$DASHBOARD_BRANCH" 2>/dev/null || echo "WARN: Failed to push dashboard to fork" # Create or update standing cross-fork PR - existing_pr=$(gh api "repos/$ORG/$ORG/pulls?head=$FORK_OWNER:$DASHBOARD_BRANCH&state=open" \ + existing_pr=$(gh api "repos/$REPORT_TARGET_REPO/pulls?head=$FORK_OWNER:$DASHBOARD_BRANCH&state=open" \ --jq '.[0].number' 2>/dev/null || echo "") pr_body="## Summary -Auto-updated by Kagenti Automation Health Dashboard. This PR is continuously updated with each generation. Merge when convenient. +Auto-updated by Rossoctl Automation Health Dashboard. This PR is continuously updated with each generation. Merge when convenient. | Metric | Value | |--------|-------| @@ -489,15 +507,19 @@ Auto-updated by Kagenti Automation Health Dashboard. This PR is continuously upd ## Related issue(s) -- kagenti/kagenti#1260" +- $MAIN_REPO#1260 + +## Automation program + +Generated by the [Rossoctl Automation Health Dashboard](https://github.com/$SOURCE_REPO/blob/main/standing-orders/health-dashboard.md)." if [ -z "$existing_pr" ] || [ "$existing_pr" = "null" ]; then - gh pr create --repo "$ORG/$ORG" \ + gh pr create --repo "$REPORT_TARGET_REPO" \ --head "$FORK_OWNER:$DASHBOARD_BRANCH" --base main \ --title "docs: Automation health dashboard (auto-updated)" \ --body "$pr_body" 2>/dev/null || echo "WARN: Failed to create dashboard PR" else - gh pr edit "$existing_pr" --repo "$ORG/$ORG" --body "$pr_body" 2>/dev/null || true + gh pr edit "$existing_pr" --repo "$REPORT_TARGET_REPO" --body "$pr_body" 2>/dev/null || true fi echo "Dashboard committed and pushed" diff --git a/scripts/dep-bump-fixer.sh b/scripts/dep-bump-fixer.sh index 69775df..2c4b486 100644 --- a/scripts/dep-bump-fixer.sh +++ b/scripts/dep-bump-fixer.sh @@ -20,7 +20,6 @@ source "$SCRIPT_DIR/program-lib.sh" # --- CLI args --- DRY_RUN=true # Safe by default ISSUE_LIMIT=15 -ORG="kagenti" VERBOSE=false SHOW_HELP=false @@ -29,7 +28,8 @@ while [[ $# -gt 0 ]]; do --dry-run) DRY_RUN=true; shift ;; --live) DRY_RUN=false; shift ;; --issue-limit) ISSUE_LIMIT="$2"; shift 2 ;; - --org) ORG="$2"; shift 2 ;; + --profile) PROFILE_FLAG="$2"; shift 2 ;; + --org) ORG_FLAG="$2"; shift 2 ;; --verbose) VERBOSE=true; shift ;; --help|-h) SHOW_HELP=true; shift ;; *) echo "Unknown option: $1"; exit 1 ;; @@ -47,7 +47,8 @@ OPTIONS: --dry-run Analyze and preview comments only (default) --live Post comments on PRs and issues --issue-limit N Process at most N issues (default: 5) - --org NAME GitHub org (default: kagenti) + --profile NAME Org profile to load (config/org..env; default org.env) + --org NAME GitHub org (default: from profile, config/org.env) --verbose Print additional diagnostic output --help, -h Show this help @@ -61,6 +62,11 @@ USAGE exit 0 fi +# Resolve org identity (--org > env > profile > default). Sets ORG, FORK_OWNER, +# MAIN_REPO, REPOS_DIR, REMAP. Reads use canonical $ORG/; write paths +# (fork PRs) derive from $ORG/$FORK_OWNER. +load_org_profile + # --- Configuration --- validate_repos_dir "${REPOS_DIR:-}" @@ -68,8 +74,11 @@ REPORTS_DIR="${REPORTS_DIR:-./reports/dep-bump}" SCAN_DATE=$(date -u +"%Y-%m-%d") SCAN_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") MAX_HISTORY_ROWS=500 -FORK_OWNER="clawgenti" -FIXER_SIGNATURE="Automated analysis by Kagenti Dep Bump Fixer" +# Marker appended to every fixer comment; also the dedup key (line 278 searches +# existing comments for it before posting). Changing this string means comments +# carrying the OLD marker are no longer recognized, so an already-analyzed PR +# may receive one duplicate comment on the next run — acceptable, one-time. +FIXER_SIGNATURE="Automated analysis by Rossoctl Dep Bump Fixer" # --- Workspace setup --- setup_workspace "dep-bump-fixer" @@ -111,7 +120,7 @@ if [ ! -f "$REPORTS_DIR/baseline.json" ]; then esac SEEN_CANON="$SEEN_CANON $canon" - gh pr list --repo "rossoctl/$canon" \ + gh pr list --repo "$ORG/$canon" \ --author "app/dependabot" \ --state merged \ --json number,createdAt,mergedAt \ @@ -179,7 +188,7 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do SEEN_CANON="$SEEN_CANON $canon" REPOS_CHECKED=$((REPOS_CHECKED + 1)) - full_repo="rossoctl/$canon" + full_repo="$ORG/$canon" issues_json=$(gh issue list --repo "$full_repo" \ --search "[dep-bump] in:title" \ @@ -554,7 +563,7 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do esac SEEN_CANON_TTM="$SEEN_CANON_TTM $canon" - gh pr list --repo "rossoctl/$canon" \ + gh pr list --repo "$ORG/$canon" \ --author "app/dependabot" \ --state merged \ --json number,createdAt,mergedAt \ diff --git a/scripts/dep-bump-scanner.sh b/scripts/dep-bump-scanner.sh index f8e878e..f10bc51 100644 --- a/scripts/dep-bump-scanner.sh +++ b/scripts/dep-bump-scanner.sh @@ -2,7 +2,7 @@ set -euo pipefail # ============================================================================= -# Dependency Bump Scanner — rossoctl org +# Dependency Bump Scanner # Monitors Dependabot PRs, classifies by severity, flags SLA breaches, # creates/closes GitHub issues, writes reports. # @@ -21,14 +21,14 @@ source "$SCRIPT_DIR/program-lib.sh" # --- CLI args --- DRY_RUN=false ISSUE_LIMIT=0 # 0 = unlimited -ORG="rossoctl" # display/report-tag only; repo reads use canonical rossoctl/ (see get_core_repos) SHOW_HELP=false while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true; shift ;; --issue-limit) ISSUE_LIMIT="$2"; shift 2 ;; - --org) ORG="$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"; exit 1 ;; esac @@ -44,7 +44,8 @@ USAGE: OPTIONS: --dry-run Scan and report only; do not create/close issues --issue-limit N Create at most N issues per run (0 = unlimited) - --org NAME GitHub org to scan (default: rossoctl) + --profile NAME Org profile to load (config/org..env; default org.env) + --org NAME GitHub org to scan (default: from profile, config/org.env) --help, -h Show this help ENVIRONMENT: @@ -64,6 +65,10 @@ USAGE exit 0 fi +# Resolve org identity (--org > env > profile > default). Sets ORG, FORK_OWNER, +# MAIN_REPO, REPOS_DIR, REMAP. Repo reads use canonical $ORG/. +load_org_profile + # --- Configuration --- validate_repos_dir "${REPOS_DIR:-}" @@ -159,7 +164,7 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do # Strip trailing comma ecosystems="${ecosystems%,}" - # Store the canonical repo name so Step 2 builds correct rossoctl/ refs. + # Store the canonical repo name so Step 2 builds correct $ORG/ refs. jq -nc --arg repo "$canon" --arg eco "$ecosystems" \ '{repo: $repo, ecosystems: ($eco | split(",") | map(select(. != "")))}' \ >> "$TMPDIR/ecosystems.jsonl" @@ -177,7 +182,7 @@ TOTAL_OPEN_PRS=0 while IFS= read -r eco_record; do repo_name=$(echo "$eco_record" | jq -r '.repo') # canonical name from Step 1 - full_repo="rossoctl/$repo_name" + full_repo="$ORG/$repo_name" echo " Checking $repo_name..." @@ -434,7 +439,7 @@ while IFS='|' read -r issue_repo issue_pr_number; do overdue=$((age_days - sla_days)) # Record repos are canonical names (see Step 1); build canonical refs. - full_repo="rossoctl/$issue_repo" + full_repo="$ORG/$issue_repo" # Deduplication search_term="[dep-bump] Stale $severity bump: $package in $issue_repo" @@ -513,7 +518,7 @@ ISSUES_CLOSED=0 while IFS='|' read -r fix_repo fix_pr_number; do [ -z "$fix_repo" ] && continue - full_repo="rossoctl/$fix_repo" + full_repo="$ORG/$fix_repo" # Find matching open issue by searching for the PR number in title/body issue_number=$(gh issue list --repo "$full_repo" \ diff --git a/scripts/link-health-fixer.sh b/scripts/link-health-fixer.sh index 17599fc..9711d7a 100755 --- a/scripts/link-health-fixer.sh +++ b/scripts/link-health-fixer.sh @@ -11,10 +11,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/program-lib.sh" # --- Configuration --- -REPOS_DIR="${REPOS_DIR:-$HOME/kagenti}" REPORTS_DIR="${REPORTS_DIR:-$HOME/workspaces/clawgenti/reports/link-scan}" -FORK_OWNER="clawgenti" -ORG="kagenti" FIX_DATE=$(date +%Y-%m-%d) SCAN_DATE=$(date +%Y-%m-%d) @@ -35,11 +32,20 @@ while [[ $# -gt 0 ]]; do --live) DRY_RUN=false; shift ;; --dry-run) DRY_RUN=true; shift ;; --issue-limit) ISSUE_LIMIT="$2"; shift 2 ;; + --profile) PROFILE_FLAG="$2"; shift 2 ;; + --org) ORG_FLAG="$2"; shift 2 ;; + --fork-owner) FORK_OWNER_FLAG="$2"; shift 2 ;; + --repos-dir) REPOS_DIR_FLAG="$2"; shift 2 ;; --verbose) VERBOSE=true; shift ;; *) echo "Unknown option: $1"; exit 1 ;; esac done +# Resolve org identity (--flag > env > profile > default). Sets ORG, FORK_OWNER, +# MAIN_REPO, REPOS_DIR, REMAP. Reads use canonical $ORG/; fork-PR write +# paths derive from $ORG/$FORK_OWNER. +load_org_profile + # --- Workspace setup --- setup_workspace "link-fixer" TMPDIR="$PROGRAM_TMPDIR" @@ -79,7 +85,7 @@ for repo_dir in "$REPOS_DIR"/*/ "$REPOS_DIR"/.github/; do esac SEEN_CANON="$SEEN_CANON $canon" - full_repo="rossoctl/$canon" + full_repo="$ORG/$canon" issues_json=$(gh issue list --repo "$full_repo" \ --search "Broken link in:title" \ @@ -383,8 +389,10 @@ while IFS= read -r item; do echo " FIX: $target_path -> $new_path" - # Determine which repo contains the source file that needs editing - source_repo_name="${repo#"$ORG/"}" + # Determine which repo contains the source file that needs editing. + # $repo is always "owner/name"; strip any owner so the bare repo name is + # correct regardless of which org owns it (do not couple to $ORG). + source_repo_name="${repo##*/}" jq -nc \ --arg number "$number" \ diff --git a/scripts/link-health-scanner.sh b/scripts/link-health-scanner.sh index 886687e..9d79306 100755 --- a/scripts/link-health-scanner.sh +++ b/scripts/link-health-scanner.sh @@ -2,7 +2,7 @@ set -euo pipefail # ============================================================================= -# Link Health Scanner — kagenti org +# Link Health Scanner # Scans all repos for broken links, creates/closes GitHub issues, writes reports. # # Usage: @@ -25,16 +25,40 @@ while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true; shift ;; --issue-limit) ISSUE_LIMIT="$2"; shift 2 ;; + --profile) PROFILE_FLAG="$2"; shift 2 ;; + --org) ORG_FLAG="$2"; shift 2 ;; + --fork-owner) FORK_OWNER_FLAG="$2"; shift 2 ;; + --repos-dir) REPOS_DIR_FLAG="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done +# Resolve org identity (--flag > env > profile > default). Sets ORG, FORK_OWNER, +# MAIN_REPO, REPOS_DIR, REMAP. Issue reads use canonical $ORG/; $MAIN_REPO +# backs the related-issue refs and the escalation URL. The report PR itself +# targets $REPORT_TARGET_REPO (see the report-destination block below). +load_org_profile + # --- Configuration --- -REPOS_DIR="${REPOS_DIR:-$HOME/kagenti}" REPORTS_DIR="${REPORTS_DIR:-$HOME/workspaces/clawgenti/reports/link-scan}" -KAGENTI_REPO="$REPOS_DIR/kagenti" -FORK_REMOTE="clawgenti-kagenti-fork" -FORK_OWNER="clawgenti" + +# Report-PR destination. The org main repo's docs/ folder feeds the docs site +# (rossoctl.dev) and cannot host machine-generated reports, so the standing +# report PR lands under automation-health/ in the automation repo. A single +# file, overwritten in place each run: trend tooling reconstructs history by +# replaying git commit parents, so we store state (not dated snapshots) and +# avoid the files-vs-diffs-on-Git anti-pattern (rossoctl/automation#44). +REPORT_TARGET_REPO="$ORG/automation" +REPORT_TARGET_NAME="${REPORT_TARGET_REPO##*/}" +REPORT_TARGET_PATH="automation-health/link-health.md" + +# Clone dir for the report target: honor an explicit MAIN_REPO_DIR override, +# else derive from REPOS_DIR. +REPORT_TARGET_DIR="${MAIN_REPO_DIR:-$REPOS_DIR/$REPORT_TARGET_NAME}" + +# Fork remote name for the report-target push. Derived from the profile so it +# carries no org literal; a stale remote of this name is corrected below. +FORK_REMOTE="$FORK_OWNER-automation-fork" SCAN_DATE=$(date -u +"%Y-%m-%d") SCAN_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") MAX_HISTORY_ROWS=500 @@ -425,17 +449,26 @@ DASHBOARD_EOF # Commit and push dashboard if [ "$DRY_RUN" = true ]; then - echo "[DRY RUN] Would push docs/link-health.md to fork and create/update cross-fork PR" + echo "[DRY RUN] Would push $REPORT_TARGET_PATH to fork and create/update cross-fork PR against $REPORT_TARGET_REPO" echo "[DRY RUN] Dashboard preview:" cat "$TMPDIR/link-health.md" else - cd "$KAGENTI_REPO" - - # Ensure fork remote exists - if ! git remote get-url "$FORK_REMOTE" &>/dev/null; then - git remote add "$FORK_REMOTE" "https://github.com/$FORK_OWNER/kagenti.git" + if [ ! -d "$REPORT_TARGET_DIR/.git" ]; then + echo "ERROR: $REPORT_TARGET_DIR does not appear to be a git repository." + echo "Export MAIN_REPO_DIR or set REPOS_DIR so $REPORT_TARGET_REPO can be found:" + echo " export MAIN_REPO_DIR=$REPOS_DIR/$REPORT_TARGET_NAME" + exit 1 fi + cd "$REPORT_TARGET_DIR" + + # Ensure the fork remote exists AND points at the current target. set-url + # corrects a stale remote (e.g. one left by a prior deployment pointing at the + # old report repo); the || add branch handles the not-yet-registered case. + fork_url="https://github.com/$FORK_OWNER/${REPORT_TARGET_NAME}.git" + git remote set-url "$FORK_REMOTE" "$fork_url" 2>/dev/null \ + || git remote add "$FORK_REMOTE" "$fork_url" + # Fetch fork's branch if it exists, otherwise create from main if git fetch "$FORK_REMOTE" link-health/reports 2>/dev/null; then git checkout -B link-health/reports "$FORK_REMOTE/link-health/reports" @@ -445,19 +478,19 @@ else || git checkout -B link-health/reports fi - mkdir -p docs - cp "$TMPDIR/link-health.md" docs/link-health.md - git add docs/link-health.md + mkdir -p "$(dirname "$REPORT_TARGET_PATH")" + cp "$TMPDIR/link-health.md" "$REPORT_TARGET_PATH" + git add "$REPORT_TARGET_PATH" git commit -s -m "docs: Update link health dashboard ($SCAN_ID)" 2>/dev/null || echo "No changes to commit" git push "$FORK_REMOTE" link-health/reports 2>/dev/null || echo "WARN: Failed to push dashboard to fork" # Create or update standing cross-fork PR - existing_pr=$(gh api "repos/kagenti/kagenti/pulls?head=$FORK_OWNER:link-health/reports&state=open" \ + existing_pr=$(gh api "repos/$REPORT_TARGET_REPO/pulls?head=$FORK_OWNER:link-health/reports&state=open" \ --jq '.[0].number' 2>/dev/null || echo "") pr_body="## Summary -Auto-updated by Kagenti Link Health Scanner. This PR is continuously updated with each scan. Merge when convenient. +Auto-updated by Rossoctl Link Health Scanner. This PR is continuously updated with each scan. Merge when convenient. | Metric | Value | |--------|-------| @@ -469,15 +502,19 @@ Auto-updated by Kagenti Link Health Scanner. This PR is continuously updated wit ## Related issue(s) -- kagenti/kagenti#1178" +- $MAIN_REPO#1178 + +## Automation program + +Generated by the [Rossoctl Link Health Scanner](https://github.com/$SOURCE_REPO/blob/main/standing-orders/link-health.md)." if [ -z "$existing_pr" ] || [ "$existing_pr" = "null" ]; then - gh pr create --repo kagenti/kagenti \ + gh pr create --repo "$REPORT_TARGET_REPO" \ --head "$FORK_OWNER:link-health/reports" --base main \ --title "docs: Link health report (auto-updated)" \ --body "$pr_body" 2>/dev/null || echo "WARN: Failed to create dashboard PR" else - gh pr edit "$existing_pr" --repo kagenti/kagenti --body "$pr_body" 2>/dev/null || true + gh pr edit "$existing_pr" --repo "$REPORT_TARGET_REPO" --body "$pr_body" 2>/dev/null || true fi fi @@ -488,7 +525,7 @@ if [ "$NEW_LINKS" -gt "$ESCALATION_THRESHOLD" ]; then echo "" echo "ALERT: Link health scan found $NEW_LINKS new broken links (threshold: $ESCALATION_THRESHOLD)." echo "This may indicate a bulk documentation change or a widespread external service outage." - echo "Review issues at https://github.com/kagenti/kagenti/issues?q=label:broken-link" + echo "Review issues at https://github.com/$MAIN_REPO/issues?q=label:broken-link" fi # --- Summary --- diff --git a/scripts/pr-review-impact.sh b/scripts/pr-review-impact.sh index 4c4c873..205885c 100755 --- a/scripts/pr-review-impact.sh +++ b/scripts/pr-review-impact.sh @@ -51,6 +51,8 @@ while [[ $# -gt 0 ]]; do --verbose) VERBOSE=true; shift ;; --dry-run) DRY_RUN=true; shift ;; --reports-dir) REPORTS_DIR="$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 @@ -74,6 +76,8 @@ Options: --dry-run Compute and print impact.json to stdout; do not write --reports-dir DIR Where to write impact.json (default: ./reports/pr-review, or $REPORTS_DIR if set in the environment) + --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 NOTES: @@ -90,6 +94,10 @@ USAGE exit 0 fi +# Resolve org identity (env > profile > default) before reading the allowlist; +# get_core_repos() prepends $ORG and fails loud if it is unset. +load_org_profile + # --- Workspace and reports setup --- setup_workspace "pr-review-impact" WORK_DIR="$PROGRAM_TMPDIR" diff --git a/scripts/pr-review-scanner.sh b/scripts/pr-review-scanner.sh index b68a39f..5372634 100755 --- a/scripts/pr-review-scanner.sh +++ b/scripts/pr-review-scanner.sh @@ -11,29 +11,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "$SCRIPT_DIR/program-lib.sh" -# --- Configuration --- -BOT_USER="clawgenti" - -# Repo coverage comes from the shared allowlist (config/core-repos.txt) via -# get_core_repos(). Build the array with a portable while-read loop (mapfile is -# bash 4+, unavailable on macOS's bash 3.2). Fail loud rather than silently -# scanning an empty set. -REPOS=() -while IFS= read -r repo_line; do - [ -n "$repo_line" ] && REPOS+=("$repo_line") -done < <(get_core_repos) - -if [ "${#REPOS[@]}" -eq 0 ]; then - echo "ERROR: core repos allowlist is empty or could not be loaded" >&2 - exit 1 -fi - -LABEL="ready-for-ai-review" -REVIEW_MARKER="