Update package version to 0.0.1-beta, add new dependencies including ExcelJS, and refactor export utilities to utilize ExcelJS for Excel file generation. Enhance component JSON files with vendor information for improved asset management.

This commit is contained in:
2026-01-07 02:12:12 +00:00
parent 4bc0fd203f
commit 97d2b66f02
33 changed files with 4394 additions and 1088 deletions

180
api/github_webhook/index.py Executable file
View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
GitHub webhook handler for detecting upstream changes.
Receives push events from GitHub and triggers vendor update checks
for affected repositories and paths.
"""
import hashlib
import hmac
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_signature(payload_body: bytes, signature_header: str, secret: str) -> bool:
"""Verify GitHub webhook signature."""
if not secret:
return False
hash_object = hmac.new(
secret.encode('utf-8'),
msg=payload_body,
digestmod=hashlib.sha256
)
expected_signature = "sha256=" + hash_object.hexdigest()
return hmac.compare_digest(expected_signature, signature_header)
def load_manifest(manifest_path: Path) -> List[Dict]:
"""Load vendor manifest."""
if not manifest_path.exists():
return []
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, list):
return data
elif isinstance(data, dict) and 'entries' in data:
return data['entries']
return []
except (json.JSONDecodeError, IOError):
return []
def find_affected_entries(push_event: Dict, manifest: List[Dict]) -> List[str]:
"""
Find manifest entries affected by a push event.
Returns list of manifest entry IDs.
"""
affected = []
repo_full_name = push_event.get('repository', {}).get('full_name')
if not repo_full_name:
return affected
commits = push_event.get('commits', [])
changed_files = set()
for commit in commits:
changed_files.update(commit.get('added', []))
changed_files.update(commit.get('modified', []))
changed_files.update(commit.get('removed', []))
# Match changed files against manifest entries
for entry in manifest:
source_repo = entry.get('source_repo')
source_path = entry.get('source_path')
if source_repo == repo_full_name and source_path in changed_files:
affected.append(entry['id'])
return affected
@app.route('/webhook', methods=['POST'])
def webhook():
"""Handle GitHub webhook POST requests."""
# Get webhook secret from environment
webhook_secret = os.getenv('WEBHOOK_SECRET')
# Verify signature if secret is configured
if webhook_secret:
signature = request.headers.get('X-Hub-Signature-256', '')
if not verify_signature(request.data, signature, webhook_secret):
return jsonify({'error': 'Invalid signature'}), 401
# Parse event
try:
event = request.json
event_type = request.headers.get('X-GitHub-Event', '')
except Exception as e:
return jsonify({'error': f'Invalid JSON: {e}'}), 400
# Only process push events
if event_type != 'push':
return jsonify({'message': f'Ignoring event type: {event_type}'}), 200
# Load manifest
script_dir = Path(__file__).parent.parent.parent
manifest_path = script_dir / 'manifest' / 'vendor_manifest.json'
manifest = load_manifest(manifest_path)
# Find affected entries
affected_ids = find_affected_entries(event, manifest)
if not affected_ids:
return jsonify({
'message': 'No affected manifest entries',
'repo': event.get('repository', {}).get('full_name')
}), 200
# Log the event
repo_name = event.get('repository', {}).get('full_name', 'unknown')
print(f"Webhook: Push event for {repo_name} affects {len(affected_ids)} entries: {affected_ids}")
# Trigger check/update flow
# In a production environment, you might want to enqueue this as a background job
# For now, we'll just log and optionally run check_updates.py
try:
# Run check_updates.py to see what needs updating
check_script = script_dir / 'scripts' / 'check_updates.py'
result = subprocess.run(
[sys.executable, str(check_script), '--manifest', str(manifest_path)],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
return jsonify({
'message': 'Update check completed',
'affected_entries': affected_ids,
'check_output': result.stdout
}), 200
else:
# Some entries are out-of-date
return jsonify({
'message': 'Updates available',
'affected_entries': affected_ids,
'check_output': result.stdout,
'stderr': result.stderr
}), 200
except subprocess.TimeoutExpired:
return jsonify({
'message': 'Update check timed out',
'affected_entries': affected_ids
}), 500
except Exception as e:
return jsonify({
'error': f'Failed to run update check: {e}',
'affected_entries': affected_ids
}), 500
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint."""
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
# For local development
port = int(os.getenv('PORT', 5000))
app.run(host='0.0.0.0', port=port, debug=os.getenv('FLASK_DEBUG', 'false').lower() == 'true')
else:
# For serverless/production deployment (e.g., AWS Lambda, Google Cloud Functions)
# Export the Flask app
pass