Files
awesome-kubernetes/src/gitops_manager.py
Nubenetes Bot 4e87c248fd feat: implement AI-powered news digest engine, MkDocs UX overhaul, pipeline hardening, and stub page merges
- Add 26-category news digest engine (src/news_digest.py) with Gemini AI ranking
  for 3/6/12 month temporal panels across tech, cloud, and geo categories
- Add discovered_at, company, geo_region fields to inventory schema with backfill
  script populating 18K+ existing entries
- Fix critical v2-mkdocs.yml bug: plugins were nested under theme (silently disabled)
- Add MkDocs Material features: instant nav, breadcrumbs, footer, announce bar
- Add trending cards CSS grid and replace Agentic Pulse with dynamic Trending Now
- Generate tech-digest.md and industry-digest.md with tabbed 3/6/12 month views
- Merge 12 stub pages (<40 lines each) into parent categories with redirects
- Replace 50 bare except:pass patterns with contextual logging across all pipeline files
- Expand autonomous discovery from 6 to 14 GitHub search queries
- Add stale health re-check for online entries older than 30 days
- Track addition_method by source type (rss, twitter, github_trending)
- Add digest generation step to CI publish workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:38:56 +02:00

199 lines
9.7 KiB
Python

import json
import base64
from github import Github
from datetime import datetime
from typing import List, Dict
from src.gemini_utils import SESSION_TRACKER
class RepositoryController:
def __init__(self, access_token: str, repository_identifier: str):
self.github_client = Github(access_token)
self.repository = self.github_client.get_repo(repository_identifier)
# Force 'develop' as the primary target for all PRs and base for feature branches
self.default_branch_name = "develop"
def _create_feature_branch(self, branch_name: str) -> None:
base_branch = self.repository.get_branch(self.default_branch_name)
self.repository.create_git_ref(ref=f"refs/heads/{branch_name}", sha=base_branch.commit.sha)
def get_file_from_branch(self, file_path: str, branch_name: str) -> str:
try:
file_meta = self.repository.get_contents(file_path, ref=branch_name)
return base64.b64decode(file_meta.content).decode("utf-8")
except Exception as e:
print(f"[WARN] Failed to get file '{file_path}' from branch '{branch_name}': {str(e)[:100]}")
return ""
def apply_historical_chunk(self, updates: dict, next_since: str) -> None:
branch_name = "bot/historical-accumulator"
try:
self.repository.get_branch(branch_name)
except Exception as e:
print(f"[WARN] Branch '{branch_name}' not found, creating: {str(e)[:100]}")
self._create_feature_branch(branch_name)
for file_path, content in updates.items():
try:
try:
file_meta = self.repository.get_contents(file_path, ref=branch_name)
self.repository.update_file(
path=file_path, message=f"chore(historical): chunk sync since {next_since}",
content=content, sha=file_meta.sha, branch=branch_name
)
except Exception as e:
if "404" in str(e):
self.repository.create_file(
path=file_path, message=f"chore(historical): init {file_path}",
content=content, branch=branch_name
)
except Exception as e:
print(f"Error in historical chunk for {file_path}: {e}")
def apply_multi_file_changes(self, updates: dict, metrics: dict, safety_report: str = "") -> str:
timestamp_slug = datetime.now().strftime("%Y%m%d-%H%M")
branch_name = f"bot/knowledge-update-{timestamp_slug}"
try:
self._create_feature_branch(branch_name)
except Exception as e:
print(f"[WARN] Branch creation failed, retrying with unique suffix: {str(e)[:100]}")
branch_name = f"bot/knowledge-update-{timestamp_slug}-{id(updates)}"
self._create_feature_branch(branch_name)
if not updates:
updates["src/memory/last_audit_run.json"] = json.dumps({
"timestamp": metrics.get("end_date"),
"total_extracted": metrics.get("total_extracted"),
"status": "Audit Only"
}, indent=2)
for file_path, content in updates.items():
if content is None:
try:
file_meta = self.repository.get_contents(file_path, ref=branch_name)
self.repository.delete_file(file_path, f"chore: remove {file_path}", file_meta.sha, branch=branch_name)
except Exception: pass
continue
try:
commit_signature = f"chore: update {file_path} [{timestamp_slug}]"
try:
file_meta = self.repository.get_contents(file_path, ref=branch_name)
self.repository.update_file(
path=file_path, message=commit_signature, content=content,
sha=file_meta.sha, branch=branch_name
)
except Exception as e:
if "404" in str(e):
self.repository.create_file(
path=file_path, message=f"chore: create {file_path}",
content=content, branch=branch_name
)
except Exception as e:
print(f"Error processing {file_path}: {e}")
# --- REPORT CONSTRUCTION ---
full_report = metrics.get('full_report', [])
sorted_report = sorted(full_report, key=lambda x: 0 if x['status'] == 'INCLUDED' else (1 if x['status'] == 'DUPLICATE' else 2))
counts = {"INCLUDED": 0, "DUPLICATE": 0, "FILTERED": 0}
source_counts = {}
all_rows = []
header_table = "| # | Status | Score | Lang | Type | Date | Source | Reason | URL |\n| :--- | :--- | :---: | :---: | :--- | :---: | :--- | :--- | :--- |\n"
for idx, item in enumerate(sorted_report, 1):
status_emoji = {"INCLUDED": "", "DUPLICATE": "👯", "FILTERED": "🛡️"}.get(item['status'], "")
date_str = str(item.get('post_date', 'N/A'))[:10] if item.get('post_date') else 'N/A'
score = item.get('impact_score', 'N/A')
lang = item.get('language', 'N/A')[:2].upper() if item.get('language') else 'EN'
res_type = item.get('type', 'Ref')
row = f"| {idx} | {status_emoji} {item['status']} | {score} | {lang} | {res_type} | {date_str} | {item.get('source', 'N/A')} | {item['reason']} | {item['url']} |\n"
all_rows.append(row)
counts[item['status']] = counts.get(item['status'], 0) + 1
src = item.get('source', 'Unknown')
source_counts[src] = source_counts.get(src, 0) + 1
# AI Intel and Mermaid
ai_intel = SESSION_TRACKER.get_intelligence_report()
source_md = "#### 📊 Source Distribution\n| Source | Count |\n| :--- | :---: |\n"
for src, count in sorted(source_counts.items(), key=lambda x: x[1], reverse=True):
source_md += f"| {src} | {count} |\n"
mermaid = f"### 📊 Decision Metrics\n<details><summary>📊 Ver Gráfico de Decisión</summary>\n\n```mermaid\npie title Agentic Decision Distribution\n \"Accepted\" : {counts['INCLUDED']}\n \"Duplicates\" : {counts['DUPLICATE']}\n \"Filtered\" : {counts['FILTERED']}\n```\n\n</details>\n"
# Build PR Body (With Safety Guard Splitting)
pr_body = f"## 💎 Knowledge Update: {datetime.now().strftime('%d %b %Y')}\n\nProcessed **{metrics.get('total_extracted', 0)}** links.\n\n"
# If safety_report is huge, move it to its own comment
safety_in_body = True
if len(safety_report) > 30000:
pr_body += "⚠️ **Detailed Safety Audit moved to comments due to scale.**\n\n"
safety_in_body = False
else:
pr_body += f"{safety_report}\n\n"
# Extract All Accepted Gems
accepted_items = [item for item in sorted_report if item['status'] == 'INCLUDED']
accepted_items.sort(key=lambda x: int(x.get('impact_score', 0)) if str(x.get('impact_score', '0')).isdigit() else 0, reverse=True)
gems_md = "### 🏆 Accepted Resources\n"
if accepted_items:
for item in accepted_items:
score = item.get('impact_score', 'N/A')
title = str(item.get('title', 'Unknown')).replace("\n", " ").strip()[:80]
gems_md += f"- **[{score}]** [{title}]({item['url']})\n"
else:
gems_md += "*No new resources accepted in this run.*\n"
pr_body += f"{ai_intel}\n\n{mermaid}\n{source_md}\n{gems_md}\n---\n**Audit Matrix and Logs follow in successive comments.**\n"
pr = self.repository.create_pull(
title=f"💎 Knowledge Update & Optimization: {datetime.now().strftime('%d %b %Y')}",
body=pr_body[:65000],
head=branch_name,
base=self.default_branch_name
)
# 1. Safety Report (if huge)
if not safety_in_body:
log_header = "## 🛡️ Safety & Mandate Audit (Detailed)\n"
current_chunk = log_header
for line in safety_report.splitlines():
if len(current_chunk) + len(line) > 60000:
pr.create_issue_comment(current_chunk)
current_chunk = log_header + line + "\n"
else:
current_chunk += line + "\n"
pr.create_issue_comment(current_chunk)
# 2. X.com Extraction Audit Trail
x_audit = metrics.get('x_audit', [])
if x_audit:
log_header = "### 📜 Extraction Audit Trail\n*Detailed logs of social and RSS discovery attempts.*\n\n"
current_log = log_header
for entry in x_audit:
if len(current_log) + len(entry) > 60000:
pr.create_issue_comment(current_log)
current_log = log_header + entry + "\n"
else:
current_log += entry + "\n"
pr.create_issue_comment(current_log)
# 3. Split Audit Matrix into Comments
current_comment = header_table
part = 1
for row in all_rows:
if len(current_comment) + len(row) > 60000:
pr.create_issue_comment(f"### 📋 Audit Matrix (Part {part})\n<details><summary>📋 Clic para expandir Audit Matrix</summary>\n\n{current_comment}\n\n</details>")
current_comment = header_table + row
part += 1
else:
current_comment += row
pr.create_issue_comment(f"### 📋 Audit Matrix (Part {part})\n<details><summary>📋 Clic para expandir Audit Matrix</summary>\n\n{current_comment}\n\n</details>")
return pr.html_url