Shiro Daily Scan #119
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Shiro Daily Scan | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "0 12 * * *" | |
| permissions: | |
| contents: read | |
| jobs: | |
| gemini-warden: | |
| name: Gemini Warden Daily Scan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Run Gemini warden scan | |
| env: | |
| GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | |
| GEMINI_MODEL: gemini-2.5-flash | |
| GEMINI_CONTEXT_PATH: GEMINI.md | |
| GEMINI_REPORT_PATH: logs/health_checks/cloud_wardens/gemini_warden_status.json | |
| GEMINI_MANIFEST_PATH: logs/health_checks/cloud_wardens/gemini_warden_manifest.json | |
| run: | | |
| python3 - <<'PY' | |
| import hashlib | |
| import json | |
| import os | |
| import subprocess | |
| import traceback | |
| import urllib.error | |
| import urllib.request | |
| from collections import Counter | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| repo = Path(os.environ["GITHUB_WORKSPACE"]).resolve() | |
| report_path = repo / os.environ["GEMINI_REPORT_PATH"] | |
| manifest_path = repo / os.environ["GEMINI_MANIFEST_PATH"] | |
| report_path.parent.mkdir(parents=True, exist_ok=True) | |
| def utcnow() -> str: | |
| return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") | |
| def write_report(payload: dict) -> None: | |
| report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") | |
| def list_files() -> list[str]: | |
| result = subprocess.run( | |
| ["git", "ls-files", "-z"], | |
| cwd=repo, | |
| check=True, | |
| capture_output=True, | |
| ) | |
| return [item for item in result.stdout.decode("utf-8").split("\0") if item] | |
| def sha256_for(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def build_inventory(files: list[str]) -> tuple[dict, dict]: | |
| manifest = {} | |
| extensions = Counter() | |
| top_levels = Counter() | |
| total_bytes = 0 | |
| for rel_path in files: | |
| relative_path = Path(rel_path) | |
| path = repo / relative_path | |
| if not path.is_file(): | |
| continue | |
| manifest[rel_path] = sha256_for(path) | |
| suffix = path.suffix or "<no_ext>" | |
| extensions[suffix] += 1 | |
| top_levels[relative_path.parts[0] if relative_path.parts else "."] += 1 | |
| total_bytes += path.stat().st_size | |
| summary = { | |
| "timestamp": utcnow(), | |
| "repository": os.environ.get("GITHUB_REPOSITORY"), | |
| "ref": os.environ.get("GITHUB_REF"), | |
| "commit": os.environ.get("GITHUB_SHA"), | |
| "file_count": len(manifest), | |
| "total_bytes": total_bytes, | |
| "top_level_counts": dict(top_levels.most_common(20)), | |
| "extension_counts": dict(extensions.most_common(20)), | |
| "sample_files": list(sorted(manifest))[:200], | |
| } | |
| return manifest, summary | |
| def extract_text(response_json: dict) -> str: | |
| candidates = response_json.get("candidates") or [] | |
| if not candidates: | |
| return "" | |
| parts = candidates[0].get("content", {}).get("parts") or [] | |
| return "".join(part.get("text", "") for part in parts).strip() | |
| def extract_json_block(text: str): | |
| candidate = text.strip() | |
| if candidate.startswith("```"): | |
| lines = candidate.splitlines() | |
| if lines and lines[0].startswith("```"): | |
| lines = lines[1:] | |
| if lines and lines[-1].startswith("```"): | |
| lines = lines[:-1] | |
| candidate = "\n".join(lines).strip() | |
| start = candidate.find("{") | |
| end = candidate.rfind("}") | |
| if start == -1 or end == -1 or end < start: | |
| return None | |
| try: | |
| return json.loads(candidate[start : end + 1]) | |
| except json.JSONDecodeError: | |
| return None | |
| def query_gemini(prompt: str) -> tuple[dict | None, str]: | |
| api_key = os.environ.get("GEMINI_API_KEY") | |
| if not api_key: | |
| raise RuntimeError("GEMINI_API_KEY is not configured") | |
| model = os.environ["GEMINI_MODEL"] | |
| url = ( | |
| "https://generativelanguage.googleapis.com/v1beta/models/" | |
| f"{model}:generateContent?key={api_key}" | |
| ) | |
| payload = { | |
| "contents": [ | |
| { | |
| "parts": [ | |
| { | |
| "text": prompt | |
| } | |
| ] | |
| } | |
| ], | |
| "generationConfig": { | |
| "temperature": 0.1, | |
| "responseMimeType": "application/json" | |
| } | |
| } | |
| request = urllib.request.Request( | |
| url, | |
| data=json.dumps(payload).encode("utf-8"), | |
| headers={"Content-Type": "application/json"}, | |
| method="POST", | |
| ) | |
| with urllib.request.urlopen(request, timeout=120) as response: | |
| response_json = json.loads(response.read().decode("utf-8")) | |
| response_text = extract_text(response_json) | |
| return extract_json_block(response_text), response_text | |
| try: | |
| files = list_files() | |
| manifest, summary = build_inventory(files) | |
| manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8") | |
| context = (repo / os.environ["GEMINI_CONTEXT_PATH"]).read_text(encoding="utf-8") | |
| prompt = ( | |
| f"{context}\n\n" | |
| "Task: Act as the daily Gemini steward/warden for this repository. " | |
| "Analyze the repository summary below and return strict JSON only.\n\n" | |
| f"Repository summary:\n{json.dumps(summary, indent=2)}\n" | |
| ) | |
| parsed, raw_text = query_gemini(prompt) | |
| report = { | |
| "warden_name": "gemini_warden", | |
| "runtime": "github_actions", | |
| "status": "healthy", | |
| "timestamp": utcnow(), | |
| "workflow": "shiro-daily-scan", | |
| "model_name": os.environ["GEMINI_MODEL"], | |
| "scan": summary, | |
| "findings": [], | |
| "issues": [], | |
| "recommendations": [], | |
| "raw_response": raw_text, | |
| } | |
| if isinstance(parsed, dict): | |
| report["status"] = parsed.get("status", "healthy") | |
| report["summary"] = parsed.get("summary", "") | |
| report["findings"] = parsed.get("findings", []) | |
| report["issues"] = parsed.get("issues", []) | |
| report["recommendations"] = parsed.get("recommendations", []) | |
| else: | |
| report["status"] = "warning" | |
| report["issues"] = ["Gemini response was not valid JSON"] | |
| report["recommendations"] = [ | |
| "Inspect the raw_response field and adjust the prompt if needed" | |
| ] | |
| write_report(report) | |
| except Exception as exc: | |
| failure_report = { | |
| "warden_name": "gemini_warden", | |
| "runtime": "github_actions", | |
| "status": "critical", | |
| "timestamp": utcnow(), | |
| "workflow": "shiro-daily-scan", | |
| "model_name": os.environ.get("GEMINI_MODEL", "gemini-2.5-flash"), | |
| "scan": { | |
| "repository": os.environ.get("GITHUB_REPOSITORY"), | |
| "ref": os.environ.get("GITHUB_REF"), | |
| "commit": os.environ.get("GITHUB_SHA"), | |
| }, | |
| "findings": [], | |
| "issues": [str(exc)], | |
| "recommendations": [ | |
| "Review the workflow logs and rerun the Gemini warden scan" | |
| ], | |
| "traceback": traceback.format_exc(), | |
| } | |
| write_report(failure_report) | |
| if not manifest_path.exists(): | |
| manifest_path.write_text("{}", encoding="utf-8") | |
| PY | |
| - name: Show Gemini warden report | |
| if: always() | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| from pathlib import Path | |
| report_path = Path("logs/health_checks/cloud_wardens/gemini_warden_status.json") | |
| if report_path.exists(): | |
| print(json.dumps(json.loads(report_path.read_text(encoding="utf-8")), indent=2)) | |
| else: | |
| print("Gemini warden report missing") | |
| PY | |
| - name: Upload Gemini warden artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: gemini-warden-${{ github.run_id }} | |
| path: | | |
| logs/health_checks/cloud_wardens/gemini_warden_status.json | |
| logs/health_checks/cloud_wardens/gemini_warden_manifest.json | |
| if-no-files-found: error | |
| # Keep several daily runs available for download while health checks only | |
| # require the latest artifact to be fresher than 36 hours. | |
| retention-days: 30 |