/
githubmirror
/
hwdata
Обзор
Документация
Войти
/
githubmirror
/
hwdata
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
monthly-update.py
502 строки
16 KB
Vitezslav Crhonek
Use -test suffix for --local-only branch names
03 июл 2026, 10:02
03 июл 2026, 10:02
d1f6971
Код
Авторство
О чём код?
#!/usr/bin/env python3 # pylint: disable=invalid-name """ Automated monthly hwdata update script. This script automates the monthly update workflow: 1. Creates a feature branch (e.g., apr-update) 2. Downloads latest ID files 3. Updates hwdata.spec 4. Creates commits 5. Pushes and creates a PR Usage: ./monthly-update.py # Full automation ./monthly-update.py --local-only # Test locally without push/PR """ import argparse import subprocess import sys import tempfile from datetime import datetime from pathlib import Path class Colors: # pylint: disable=too-few-public-methods """Terminal colors for output.""" HEADER = '\033[95m' OKBLUE = '\033[94m' OKCYAN = '\033[96m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' def log(message, color=None): """Print colored log message.""" if color: print(f"{color}{message}{Colors.ENDC}") else: print(message) def run_command(cmd, check=True, capture=True): """Run shell command and return output. Note: Uses shell=True for convenience with git/make commands. All command strings are constructed internally, not from user input. """ log(f"Running: {cmd}", Colors.OKCYAN) result = subprocess.run( cmd, shell=True, # Safe here: all commands are hardcoded, no user input capture_output=capture, text=True, check=False ) if check and result.returncode != 0: log(f"Command failed with exit code {result.returncode}", Colors.FAIL) if result.stderr: log(f"Error: {result.stderr}", Colors.FAIL) sys.exit(1) return result def get_current_branch(): """Get current git branch name.""" result = run_command("git rev-parse --abbrev-ref HEAD") return result.stdout.strip() def get_month_name(): """Get current month name in lowercase (e.g., 'apr').""" return datetime.now().strftime("%b").lower() def check_clean_working_tree(): """Ensure working tree is clean.""" result = run_command("git status --porcelain") if result.stdout.strip(): log("Error: Working tree is not clean. Commit or stash changes first.", Colors.FAIL) sys.exit(1) def create_update_branch(month, local_only=False): """Create and checkout monthly update branch.""" branch_name = f"{month}-update" if local_only: branch_name += "-test" log(f"\n📝 Creating branch: {branch_name}", Colors.HEADER) # Check if branch already exists result = run_command(f"git rev-parse --verify {branch_name}", check=False) if result.returncode == 0: if local_only: # In test mode, fail if test branch exists - don't reuse it log(f"Error: Test branch {branch_name} already exists.", Colors.FAIL) log(f"Delete it first: git branch -D {branch_name}", Colors.FAIL) sys.exit(1) else: # In normal mode, allow resuming existing branch log(f"Branch {branch_name} already exists. Checking it out.", Colors.WARNING) run_command(f"git checkout {branch_name}") else: run_command(f"git checkout -b {branch_name}") return branch_name def download_files(): """Download latest ID files.""" log("\n⬇️ Downloading latest ID files...", Colors.HEADER) # Check if pnp.ids.csv exists if not Path("pnp.ids.csv").exists(): log("⚠️ pnp.ids.csv not found!", Colors.WARNING) log("Please manually download from https://uefi.org/uefi-pnp-export", Colors.WARNING) log("and save as pnp.ids.csv, then run this script again.", Colors.WARNING) sys.exit(1) # Run configure run_command("./configure") # Run make download result = run_command("make download", check=False) if result.returncode != 0: log("Warning: make download had issues, but continuing...", Colors.WARNING) log("✅ Download complete", Colors.OKGREEN) def update_usb_patches(): """Check if USB patches need updating and regenerate them if needed.""" log("\n🔧 Checking USB patches...", Colors.HEADER) # Check if patches apply cleanly (no offset warnings) result = run_command( "patch --dry-run -p1 -o /dev/null usb.ids.converted " "01-utf-8-encoding.patch.patch 2>&1 | grep -i offset", check=False ) has_offset = result.returncode == 0 and result.stdout.strip() if has_offset: log(" Patches have offsets, regenerating...", Colors.WARNING) # Regenerate patches using make target result = run_command("make update-usb-patches 2>&1", check=False) if result.returncode != 0: log(" Failed to regenerate patches", Colors.FAIL) return False # Verify .new files were created patch1_new = Path("01-utf-8-encoding.patch.patch.new") patch2_new = Path("02-typos.patch.patch.new") if not (patch1_new.exists() and patch2_new.exists()): log(" Patch generation failed - .new files not created", Colors.FAIL) return False # Move .new files to replace originals patch1_new.rename("01-utf-8-encoding.patch.patch") patch2_new.rename("02-typos.patch.patch") log(" ✅ Patches regenerated with zero offset", Colors.OKGREEN) return True # Patches were updated else: log(" ✅ Patches apply cleanly", Colors.OKGREEN) return False # No update needed def validate_files(): """Run make check to validate files.""" log("\n✅ Validating ID files...", Colors.HEADER) result = run_command("make check", check=False, capture=False) if result.returncode != 0: log("Validation failed! Fix errors before continuing.", Colors.FAIL) sys.exit(1) log("✅ Validation passed", Colors.OKGREEN) def get_file_dates(): """Extract dates from pci.ids and usb.ids headers.""" dates = {} # PCI date result = run_command("grep 'Date:' pci.ids | head -n1 | cut -d' ' -f5") dates['pci'] = result.stdout.strip() if result.stdout else 'unknown' # USB date result = run_command("grep 'Date:' usb.ids | head -n1 | cut -d' ' -f6") dates['usb'] = result.stdout.strip() if result.stdout else 'unknown' return dates def get_pci_stats(): """Get PCI changes statistics using compare-pci-ids.py.""" # Save current pci.ids as old if not Path("pci.ids.old").exists(): result = run_command("git show HEAD:pci.ids > pci.ids.old", check=False) if result.returncode != 0: return None result = run_command("./compare-pci-ids.py pci.ids.old pci.ids 2>/dev/null", check=False) if result.returncode == 0: return result.stdout return None def detect_changes(): """Detect which files changed for commit message.""" changes = [] result = run_command("git diff --name-only pci.ids", check=False) if result.stdout.strip(): changes.append("pci") result = run_command("git diff --name-only usb.ids", check=False) if result.stdout.strip(): changes.append("usb") result = run_command("git diff --name-only oui.txt iab.txt pnp.ids", check=False) if result.stdout.strip(): changes.append("vendor ids") return changes def get_current_version(): """Get current version from hwdata.spec.""" result = run_command("awk '/Version:/ { print $2 }' hwdata.spec") return result.stdout.strip() def bump_version(current_version): """Bump minor version number.""" parts = current_version.split('.') parts[-1] = str(int(parts[-1]) + 1) return '.'.join(parts) def update_spec_file(new_version, changes): """Update hwdata.spec with new version and changelog.""" log(f"\n📝 Updating hwdata.spec to version {new_version}...", Colors.HEADER) # Read spec file with open('hwdata.spec', 'r', encoding='utf-8') as f: lines = f.readlines() # Update version for i, line in enumerate(lines): if line.startswith('Version:'): lines[i] = f'Version: {new_version}\n' break # Create changelog entry today = datetime.now().strftime("%a %b %d %Y") # Get git user info result = run_command("git config user.name") user_name = result.stdout.strip() result = run_command("git config user.email") user_email = result.stdout.strip() # Determine changelog message if changes: change_list = ", ".join(changes) changelog_msg = f"Update {change_list}" else: changelog_msg = "Update hardware IDs" # Find %changelog section changelog_index = None for i, line in enumerate(lines): if line.startswith('%changelog'): changelog_index = i break if changelog_index is not None: # Insert new entry after %changelog line new_entry = f"* {today} {user_name} <{user_email}> - {new_version}-1\n- {changelog_msg}\n\n" lines.insert(changelog_index + 1, new_entry) # Write back with open('hwdata.spec', 'w', encoding='utf-8') as f: f.writelines(lines) log(f"✅ Updated to version {new_version}", Colors.OKGREEN) def create_commit(changes): """Create git commit with appropriate message.""" log("\n💾 Creating commit...", Colors.HEADER) # Stage files files_to_add = ["hwdata.spec"] if "pci" in changes: files_to_add.append("pci.ids") if "usb" in changes: files_to_add.append("usb.ids") if "vendor ids" in changes: files_to_add.extend(["oui.txt", "iab.txt", "pnp.ids"]) # Create commit message if changes: change_list = " and ".join(changes) commit_msg = f"Update {change_list}" else: commit_msg = "Update hardware IDs" run_command(f"git add {' '.join(files_to_add)}") run_command(f"git commit -s -m '{commit_msg}'") log(f"✅ Committed: {commit_msg.split(chr(10), maxsplit=1)[0]}", Colors.OKGREEN) def create_pr(branch_name, new_version, dates, pci_stats, changes, local_only=False): """Push branch and create pull request.""" if local_only: log("\n⏭️ Skipping push and PR creation (local-only mode)", Colors.WARNING) return log(f"\n🚀 Pushing branch {branch_name}...", Colors.HEADER) run_command(f"git push -u origin {branch_name}") # Create PR description pr_title = f"Monthly update - {datetime.now().strftime('%B %Y')}" # Build updated files list based on actual changes updated_files = [] if 'pci' in changes: updated_files.append(f"- PCI IDs: {dates.get('pci', 'N/A')}") if 'usb' in changes: updated_files.append(f"- USB IDs: {dates.get('usb', 'N/A')}") if 'vendor ids' in changes: updated_files.append("- Vendor IDs (OUI/IAB/PNP): Updated") pr_body = f"""Automated monthly update of hardware IDs to version {new_version} ## Updated Files {'\n'.join(updated_files)} """ if pci_stats: pr_body += f"## PCI Changes\n```\n{pci_stats}```\n\n" pr_body += """## Testing - [ ] `make check` passes - [ ] Packit builds successful - [ ] Tests pass on all targets 🤖 Generated by monthly-update.py automation """ # Create PR using gh CLI log("\n📬 Creating pull request...", Colors.HEADER) # Write PR body to temp file (safer than escaping) with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: f.write(pr_body) body_file = f.name try: cmd = f'gh pr create --title "{pr_title}" --body-file "{body_file}"' result = run_command(cmd, check=False) finally: # Clean up temp file Path(body_file).unlink(missing_ok=True) if result.returncode != 0: log("Failed to create PR automatically. Create it manually on GitHub:", Colors.WARNING) log(f" Title: {pr_title}", Colors.WARNING) log(f" Body: {pr_body}", Colors.WARNING) else: log("✅ Pull request created!", Colors.OKGREEN) if result.stdout: log(result.stdout.strip()) def cleanup(): """Cleanup temporary files.""" if Path("pci.ids.old").exists(): Path("pci.ids.old").unlink() def main(): # pylint: disable=too-many-statements """Main execution flow.""" parser = argparse.ArgumentParser(description="Automate monthly hwdata updates") parser.add_argument("--local-only", action="store_true", help="Execute locally without pushing or creating PR (test mode)") args = parser.parse_args() local_only = args.local_only if local_only: log("🔧 LOCAL-ONLY MODE - Will make local changes but not push or create PR\n", Colors.WARNING) log("🤖 Starting monthly hwdata update automation...", Colors.HEADER) # Step 1: Check prerequisites if not Path("hwdata.spec").exists(): log("Error: Not in hwdata repository root", Colors.FAIL) sys.exit(1) # Check for gh CLI (only needed if not local-only) if not local_only: result = run_command("which gh", check=False) if result.returncode != 0: log("Error: 'gh' CLI not found. Install it from https://cli.github.com/", Colors.FAIL) sys.exit(1) # Step 2: Check clean working tree original_branch = get_current_branch() if original_branch != "master": log(f"Warning: Not on master branch (currently on {original_branch})", Colors.WARNING) response = input("Continue anyway? (y/N): ") if response.lower() != 'y': sys.exit(0) check_clean_working_tree() # Step 3: Create branch month = get_month_name() branch_name = create_update_branch(month, local_only=local_only) try: # Step 4: Download files download_files() # Step 4.5: Update USB patches if needed patches_updated = update_usb_patches() # Step 5: Validate validate_files() # Step 6: Get statistics dates = get_file_dates() pci_stats = get_pci_stats() # Step 7: Detect changes changes = detect_changes() if not changes: log("\n⚠️ No changes detected in ID files!", Colors.WARNING) log("This might mean:", Colors.WARNING) log(" - Files are already up to date", Colors.WARNING) log(" - Download failed silently", Colors.WARNING) response = input("Continue anyway? (y/N): ") if response.lower() != 'y': cleanup() sys.exit(0) # Step 8: Update spec file current_version = get_current_version() new_version = bump_version(current_version) update_spec_file(new_version, changes) # Step 9: Create commit (include patch updates if any) if patches_updated: log(" Adding updated patch files to commit", Colors.OKCYAN) run_command("git add 01-utf-8-encoding.patch.patch 02-typos.patch.patch") create_commit(changes) # Step 10: Create PR (or skip if local-only) create_pr(branch_name, new_version, dates, pci_stats, changes, local_only=local_only) # Cleanup cleanup() if local_only: log("\n✨ LOCAL CHANGES COMPLETE!", Colors.OKGREEN) log(f"\nYou are now on branch: {branch_name}", Colors.OKGREEN) log("Changes have been committed locally.", Colors.OKGREEN) log("\nTo reset and undo all changes:", Colors.WARNING) log(f" git checkout {original_branch}", Colors.WARNING) log(f" git branch -D {branch_name}", Colors.WARNING) log("\nTo push and create PR manually:", Colors.OKCYAN) log(f" git push -u origin {branch_name}", Colors.OKCYAN) log(" gh pr create", Colors.OKCYAN) else: log("\n✨ All done! Review the PR and merge when ready.", Colors.OKGREEN) log(f"Branch: {branch_name}", Colors.OKGREEN) except KeyboardInterrupt: log("\n\n⚠️ Interrupted by user", Colors.WARNING) cleanup() sys.exit(1) except Exception as e: log(f"\n❌ Error: {e}", Colors.FAIL) cleanup() raise if __name__ == "__main__": main()