Merge pull request #519 from AcreetionOS-Code/fix/ai-guardian-2026052… #508
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: AI Website Guardian | |
| on: | |
| schedule: | |
| - cron: '0 */3 * * *' # Every 3 hours | |
| workflow_dispatch: | |
| push: | |
| branches: [main] | |
| jobs: | |
| guard: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| task: [pages, api, downloads, seo, security] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Pages — check every HTML page loads | |
| if: matrix.task == 'pages' | |
| env: | |
| OPENROUTER_KEY: ${{ secrets.OPENROUTER_API_KEY }} | |
| run: | | |
| python3 << 'PYEOF' | |
| import subprocess, json, os, re | |
| base_url = "https://acreetionos.org" | |
| pages = [] | |
| for f in sorted(os.listdir('.')): | |
| if f.endswith('.html') and os.path.isfile(f): | |
| pages.append(f.replace('.html', '')) | |
| issues = [] | |
| for page in pages: | |
| url = f"{base_url}/{page}.html" | |
| try: | |
| r = subprocess.run(['curl', '-s', '-o', '/tmp/page.html', '-w', '%{http_code}', | |
| '--max-time', '10', '-L', url], capture_output=True, text=True, timeout=15) | |
| code = r.stdout.strip() | |
| if code != '200' and code != '301' and code != '302': | |
| issues.append(f"{page}: HTTP {code}") | |
| continue | |
| with open('/tmp/page.html') as f: | |
| html = f.read() | |
| # Basic checks | |
| if '<title>' not in html: issues.append(f"{page}: missing title") | |
| if '</html>' not in html: issues.append(f"{page}: truncated HTML") | |
| if '404' in html[:500] and 'Not Found' in html[:500]: issues.append(f"{page}: 404 content") | |
| except Exception as e: | |
| issues.append(f"{page}: {e}") | |
| report = {'pages_checked': len(pages), 'issues': issues, 'healthy': len(issues) == 0} | |
| with open('/tmp/guard-report.json', 'w') as f: | |
| json.dump(report, f, indent=2) | |
| if issues: | |
| print(f"Found {len(issues)} issues:") | |
| for i in issues: print(f" ❌ {i}") | |
| # AI fix | |
| if os.environ.get('OPENROUTER_KEY'): | |
| prompt = f"The AcreetionOS website has {len(issues)} page issues:\n" + "\n".join(issues) | |
| prompt += "\n\nSuggest specific fixes. For each issue, provide: FILE:, LINE:, REPLACE:, WITH:" | |
| r = subprocess.run(['curl', '-s', 'https://openrouter.ai/api/v1/chat/completions', | |
| '-H', 'Content-Type: application/json', | |
| '-H', f'Authorization: Bearer {os.environ["OPENROUTER_KEY"]}', | |
| '-H', 'HTTP-Referer: https://acreetionos.org', | |
| '-d', json.dumps({ | |
| 'model': 'meta-llama/llama-3.2-3b-instruct:free', | |
| 'messages': [{'role': 'user', 'content': prompt}], | |
| 'max_tokens': 1500 | |
| })], capture_output=True, text=True, timeout=30) | |
| try: | |
| data = json.loads(r.stdout) | |
| fixes = data['choices'][0]['message']['content'] | |
| with open('/tmp/ai-fixes.txt', 'w') as f: | |
| f.write(fixes) | |
| print(f"\nAI suggested fixes:\n{fixes[:500]}") | |
| except: pass | |
| else: | |
| print(f"✅ All {len(pages)} pages healthy!") | |
| PYEOF | |
| - name: API — test all endpoints | |
| if: matrix.task == 'api' | |
| run: | | |
| python3 << 'PYEOF' | |
| import subprocess, json | |
| endpoints = [ | |
| ('GET', '/api/cve/status'), | |
| ('GET', '/api/hosting/count'), | |
| ('GET', '/api/hosting/providers'), | |
| ('GET', '/api/mirror/best?edition=cinnamon'), | |
| ] | |
| issues = [] | |
| for method, path in endpoints: | |
| url = f"https://acreetionos.org{path}" | |
| try: | |
| r = subprocess.run(['curl', '-s', '-w', '%{http_code}', '--max-time', '10', '-X', method, url], | |
| capture_output=True, text=True, timeout=15) | |
| body = r.stdout[:-3] if r.stdout else '' | |
| code = r.stdout[-3:] if len(r.stdout) >= 3 else '000' | |
| if code not in ['200', '301', '302']: | |
| issues.append(f"{method} {path}: HTTP {code}") | |
| except Exception as e: | |
| issues.append(f"{method} {path}: {e}") | |
| print(f"Tested {len(endpoints)} API endpoints") | |
| if issues: | |
| print(f"Issues: {len(issues)}") | |
| for i in issues: print(f" ❌ {i}") | |
| else: | |
| print("✅ All endpoints healthy!") | |
| PYEOF | |
| - name: Downloads — verify all ISO links work | |
| if: matrix.task == 'downloads' | |
| run: | | |
| python3 << 'PYEOF' | |
| import subprocess, re, json | |
| with open('flash.html') as f: | |
| html = f.read() | |
| editions_match = re.search(r'const EDITIONS\s*=\s*(\[[\s\S]*?\]);', html, re.DOTALL) | |
| if not editions_match: | |
| print("Could not find EDITIONS in flash.html") | |
| exit(1) | |
| js_text = editions_match.group(1) | |
| # Convert JS object syntax to valid JSON (single quotes → double quotes) | |
| js_text = re.sub(r"'", '"', js_text) | |
| # Convert boolean values | |
| js_text = re.sub(r':\s*true', ': true', js_text) | |
| js_text = re.sub(r':\s*false', ': false', js_text) | |
| # Remove trailing commas before ] and } | |
| js_text = re.sub(r',\s*([\]}])', r'\1', js_text) | |
| editions = json.loads(js_text) | |
| issues = [] | |
| for ed in editions: | |
| url = ed.get('iso_url', '') | |
| if not url: continue | |
| try: | |
| r = subprocess.run(['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '10', '-I', url], | |
| capture_output=True, text=True, timeout=15) | |
| code = r.stdout.strip() | |
| if code in ['200', '206', '301', '302']: | |
| continue # Healthy | |
| elif code in ['401', '403']: | |
| # Auth-protected — might still be valid (R2 buckets) | |
| print(f" 🔒 {ed['id']}: HTTP {code} (may be valid)") | |
| elif code in ['404']: | |
| issues.append(f"{ed['id']}: HTTP {code} NOT FOUND — {url[:80]}") | |
| elif code in ['429']: | |
| print(f" ⏳ {ed['id']}: HTTP {code} rate limited (retry later)") | |
| else: | |
| issues.append(f"{ed['id']}: HTTP {code} — {url[:80]}") | |
| except Exception as e: | |
| issues.append(f"{ed['id']}: {e}") | |
| print(f"Checked {len(editions)} edition ISO links") | |
| if issues: | |
| print(f"Broken: {len(issues)}") | |
| for i in issues: print(f" ❌ {i}") | |
| # AI fix using OpenRouter | |
| api_key = os.environ.get('OPENROUTER_KEY') | |
| if api_key and issues: | |
| prompt = f"Fix these broken AcreetionOS ISO download URLs:\n" + "\n".join(issues) | |
| prompt += "\nSuggest correct replacement URLs from archive.org, sourceforge, or github releases." | |
| r = subprocess.run(['curl', '-s', 'https://openrouter.ai/api/v1/chat/completions', | |
| '-H', 'Content-Type: application/json', | |
| '-H', f'Authorization: Bearer {api_key}', | |
| '-H', 'HTTP-Referer: https://acreetionos.org', | |
| '-d', json.dumps({ | |
| 'model': 'meta-llama/llama-3.2-3b-instruct:free', | |
| 'messages': [{'role': 'user', 'content': prompt}], | |
| 'max_tokens': 1000 | |
| })], capture_output=True, text=True, timeout=30) | |
| with open('/tmp/download-fixes.txt', 'w') as f: | |
| f.write(r.stdout) | |
| print(f"\nAI suggestions saved to /tmp/download-fixes.txt") | |
| else: | |
| print("✅ All ISO download links healthy!") | |
| PYEOF | |
| - name: SEO — check meta tags | |
| if: matrix.task == 'seo' | |
| run: | | |
| python3 << 'PYEOF' | |
| import os, re | |
| issues = [] | |
| for f in sorted(os.listdir('.')): | |
| if not f.endswith('.html') or not os.path.isfile(f): continue | |
| with open(f) as fh: | |
| html = fh.read() | |
| if '<title>' not in html: issues.append(f + ': no title') | |
| if 'name="description"' not in html and "name='description'" not in html: issues.append(f + ': no description') | |
| if 'rel="canonical"' not in html: issues.append(f + ': no canonical') | |
| if 'rel="icon"' not in html and "rel='icon'" not in html: issues.append(f + ': no icon') | |
| if 'og:title' not in html: issues.append(f + ': no OG title') | |
| if 'twitter:card' not in html: issues.append(f + ': no Twitter card') | |
| if issues: | |
| print(f"SEO issues: {len(issues)}") | |
| for i in issues: print(f" ⚠ {i}") | |
| else: | |
| print("✅ All SEO checks passed!") | |
| PYEOF | |
| - name: Create PR with findings via Worker | |
| if: failure() | |
| run: | | |
| python3 << 'PYEOF' | |
| import json, os, re, glob, urllib.request | |
| # Collect all issues from the various task outputs | |
| issues = [] | |
| fixes_applied = [] | |
| # Check for download fixes | |
| for fixfile in ['/tmp/ai-fixes.txt', '/tmp/download-fixes.txt']: | |
| if os.path.exists(fixfile): | |
| with open(fixfile) as f: | |
| content = f.read() | |
| replacements = re.findall(r'REPLACE:\s*(.+?)\s*WITH:\s*(.+?)(?=REPLACE:|\Z)', content, re.DOTALL) | |
| for old_url, new_url in replacements: | |
| old_url = old_url.strip() | |
| new_url = new_url.strip() | |
| for html_file in glob.glob('*.html'): | |
| with open(html_file) as f: | |
| data = f.read() | |
| if old_url in data: | |
| data = data.replace(old_url, new_url) | |
| with open(html_file, 'w') as f: | |
| f.write(data) | |
| fixes_applied.append(f"{html_file}: {old_url[:60]} → {new_url[:60]}") | |
| issues.append(f"Fixed URL in {html_file}") | |
| # Collect all changed files | |
| files_to_pr = [] | |
| for html_file in glob.glob('*.html'): | |
| with open(html_file) as f: | |
| content = f.read() | |
| files_to_pr.append({"path": html_file, "content": content}) | |
| if not issues: | |
| issues.append("Unknown issue — check workflow logs for details") | |
| branch = f"fix/ai-guardian-{os.popen('date +%Y%m%d-%H%M%S').read().strip()}" | |
| pr_body = "🤖 **AI Website Guardian Report**\n\n" | |
| pr_body += f"**Issues detected:** {len(issues)}\n\n" | |
| for i in issues: | |
| pr_body += f"- ❌ {i}\n" | |
| if fixes_applied: | |
| pr_body += "\n**Fixes applied:**\n" | |
| for f in fixes_applied: | |
| pr_body += f"- ✅ {f}\n" | |
| else: | |
| pr_body += "\n**Fixes applied:** ⚠ None — manual review needed\n" | |
| pr_body += "\n---\n_Created automatically by the AI Website Guardian workflow._" | |
| payload = json.dumps({ | |
| "branch": branch, | |
| "title": f"Website Guardian: {len(issues)} issue(s) found", | |
| "body": pr_body, | |
| "files": files_to_pr | |
| }).encode() | |
| req = urllib.request.Request( | |
| "https://acreetionos.org/api/github/create-pr", | |
| data=payload, | |
| headers={"Content-Type": "application/json"}, | |
| method="POST" | |
| ) | |
| try: | |
| with urllib.request.urlopen(req, timeout=30) as resp: | |
| result = json.loads(resp.read()) | |
| if result.get('pr'): | |
| print(f"✅ PR created: {result['pr']['url']}") | |
| else: | |
| print(f"⚠ {result.get('error', 'unknown')}") | |
| except urllib.error.HTTPError as e: | |
| print(f"❌ HTTP {e.code}: {e.read().decode()[:300]}") | |
| except Exception as e: | |
| print(f"❌ {e}") | |
| PYEOF |