Refactor GitHub automation tool:

- Updated WorkItemFieldExtractor to be more generic and removed Azure DevOps specific references.
- Removed the EnhancedContentBuilders class as it was specific to Azure DevOps.
- Deleted work_item_processor.py as it was no longer needed.
- Introduced workflow.py to manage GitHub workflow items (issues and pull requests) with improved structure and functionality.
- Enhanced logging and error handling across the new workflow management system.
This commit is contained in:
b-tsammmons
2025-11-11 22:46:02 -10:00
parent d6da461d10
commit 0f41a3e750
15 changed files with 2003 additions and 1446 deletions
+452 -108
View File
@@ -5,6 +5,7 @@ GUI for configuring application settings
import tkinter as tk
import threading
import subprocess
from tkinter import ttk, messagebox, scrolledtext
from typing import Dict, Any, Optional
import sys
@@ -46,7 +47,7 @@ class SettingsDialog:
# Create tabs
self._create_general_tab(notebook)
self._create_ai_tab(notebook)
self._create_dataverse_tab(notebook)
# Removed: self._create_dataverse_tab(notebook) - Azure DevOps/Dataverse specific
# Buttons frame
buttons_frame = ttk.Frame(main_frame)
@@ -83,17 +84,10 @@ class SettingsDialog:
scrollable_frame.columnconfigure(1, weight=1)
current_row = 0
# Azure DevOps section
self._create_section_header(scrollable_frame, current_row, "🔷 Azure DevOps Configuration")
current_row += 1
self._create_label_entry(scrollable_frame, current_row, "Query URL:", 'AZURE_DEVOPS_QUERY', width=60, multiline=True)
current_row += 1
self._create_label_entry(scrollable_frame, current_row, "Personal Access Token:", 'AZURE_DEVOPS_PAT', password=True, width=60)
current_row += 1
# REMOVED: Azure DevOps Configuration section
# This was specific to Azure DevOps integration
# GitHub section
self._create_section_header(scrollable_frame, current_row, "🐙 GitHub Configuration")
current_row += 1
@@ -101,9 +95,9 @@ class SettingsDialog:
self._create_label_entry(scrollable_frame, current_row, "Personal Access Token:", 'GITHUB_PAT', password=True, width=60)
current_row += 1
self._create_label_entry(scrollable_frame, current_row, "Target Repository (owner/repo):", 'GITHUB_REPO', width=60)
self._create_target_repo_dropdown(scrollable_frame, current_row)
current_row += 1
self._create_forked_repo_dropdown(scrollable_frame, current_row)
current_row += 1
@@ -148,14 +142,13 @@ class SettingsDialog:
# Help text
help_text = ttk.Label(scrollable_frame, text="💡 Getting Started:\n"
"1. Set your Azure DevOps Query URL (copy from browser)\n"
"2. Create Personal Access Tokens for both services\n"
"3. Configure GitHub repositories:\n"
"1. Create a GitHub Personal Access Token\n"
"2. Configure GitHub repositories:\n"
" • Target Repository: Where PRs will be created\n"
" • Forked Repository: Your fork where changes are made\n"
"4. Set Local Repo Path for automatic repository detection\n"
"5. Configure AI provider in the AI tab (optional)\n"
"6. Test your connection before processing items",
"3. Set Local Repo Path for automatic repository detection\n"
"4. Configure AI provider in the AI tab (optional)\n"
"5. Test your connection before processing items",
font=('Arial', 9), foreground='blue', justify=tk.LEFT, wraplength=850)
help_text.grid(row=current_row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(20, 30), padx=10)
@@ -193,7 +186,7 @@ class SettingsDialog:
self.ai_provider_var = tk.StringVar(value=self.config.get('AI_PROVIDER', 'none'))
provider_dropdown = ttk.Combobox(scrollable_frame, textvariable=self.ai_provider_var,
values=['none', 'claude', 'chatgpt', 'github-copilot'], state='readonly', width=47)
values=['none', 'claude', 'chatgpt', 'github-copilot', 'ollama'], state='readonly', width=47)
provider_dropdown.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=10)
self.entries['AI_PROVIDER'] = self.ai_provider_var
@@ -202,6 +195,13 @@ class SettingsDialog:
self._create_label_entry(scrollable_frame, 3, "ChatGPT API Key:", 'OPENAI_API_KEY', password=True)
self._create_label_entry(scrollable_frame, 4, "GitHub Token (for Copilot) [defaults to GitHub PAT]:", 'GITHUB_TOKEN', password=True)
# Ollama Configuration
self._create_label_entry(scrollable_frame, 5, "Ollama Server URL:", 'OLLAMA_URL')
self._create_label_entry(scrollable_frame, 6, "Ollama API Key (optional):", 'OLLAMA_API_KEY', password=True)
# Ollama Model Dropdown
self._create_ollama_model_dropdown(scrollable_frame, 7)
# Help text
help_text = ttk.Label(scrollable_frame, text="\n💡 Tips:\n"
"• Provider: Choose 'none' to disable AI (uses Copilot workflow)\n"
@@ -209,58 +209,21 @@ class SettingsDialog:
"• ChatGPT: Get key at platform.openai.com/api-keys\n"
"• GitHub Copilot: Uses GitHub Models API (requires GitHub token)\n"
"• GitHub Token: Auto-defaults to GitHub PAT if left empty\n"
"Cost: ~$0.01-0.05 per PR with AI, free with 'none'\n"
"Ollama: Self-hosted AI (requires Ollama server running)\n"
"• Cost: ~$0.01-0.05 per PR with AI, free with 'none' and Ollama\n"
"• AI providers clone repos locally to make changes before pushing",
font=('Arial', 9), foreground='blue', justify=tk.LEFT, wraplength=800)
help_text.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20, padx=10)
help_text.grid(row=8, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20, padx=10)
# Pack canvas and scrollbar
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def _create_dataverse_tab(self, notebook):
"""Create Dataverse/PowerApp settings tab"""
dataverse_frame = ttk.Frame(notebook)
notebook.add(dataverse_frame, text="UUF/Dataverse")
# Scrollable frame
canvas = tk.Canvas(dataverse_frame)
scrollbar = ttk.Scrollbar(dataverse_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Dataverse section
self._create_section_header(scrollable_frame, 0, "📊 PowerApp/Dataverse Configuration")
self._create_label_entry(scrollable_frame, 1, "Environment URL:", 'DATAVERSE_ENVIRONMENT_URL', width=60, multiline=True)
self._create_label_entry(scrollable_frame, 2, "Table Name:", 'DATAVERSE_TABLE_NAME')
# Azure AD section
self._create_section_header(scrollable_frame, 3, "🔐 Azure AD Configuration")
self._create_label_entry(scrollable_frame, 4, "Client ID:", 'AZURE_AD_CLIENT_ID', width=60)
self._create_label_entry(scrollable_frame, 5, "Client Secret:", 'AZURE_AD_CLIENT_SECRET', password=True, width=60)
self._create_label_entry(scrollable_frame, 6, "Tenant ID:", 'AZURE_AD_TENANT_ID', width=60)
# Help text
help_text = ttk.Label(scrollable_frame, text="\n💡 UUF Integration:\n"
"• This section is only needed if you want to fetch UUF items\n"
"• UUF items are processed differently than Azure DevOps work items\n"
"• Environment URL: Your Dataverse environment\n"
"• Azure AD app must have appropriate permissions\n"
"• Contact your PowerApp administrator for these values\n"
"• Leave blank if not using UUF integration",
font=('Arial', 9), foreground='blue', justify=tk.LEFT, wraplength=800)
help_text.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20, padx=10)
# Pack canvas and scrollbar
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# REMOVED: _create_dataverse_tab method
# This was specific to Azure DevOps/Dataverse integration
# def _create_dataverse_tab(self, notebook):
# """Create Dataverse/PowerApp settings tab"""
# ...
def _create_section_header(self, parent, row: int, text: str):
"""Create a section header"""
@@ -318,7 +281,49 @@ class SettingsDialog:
self.entries[config_key] = entry
parent.columnconfigure(1, weight=1)
def _create_target_repo_dropdown(self, parent, row: int):
"""Create target repository dropdown with search functionality"""
ttk.Label(parent, text="Target Repository:", font=('Arial', 10, 'bold')).grid(
row=row, column=0, sticky=tk.W, pady=5, padx=10)
# Frame for dropdown, search entry, and buttons
dropdown_frame = ttk.Frame(parent)
dropdown_frame.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=10)
dropdown_frame.columnconfigure(0, weight=1)
# Placeholder for target repos
self.target_repos = []
# Combobox for target repo (searchable)
self.target_repo_var = tk.StringVar(value=self.config.get('GITHUB_REPO', ''))
self.target_repo_dropdown = ttk.Combobox(dropdown_frame, textvariable=self.target_repo_var,
values=[''], width=50)
self.target_repo_dropdown.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
self.entries['GITHUB_REPO'] = self.target_repo_var
# Bind typing event for search
self.target_repo_dropdown.bind('<KeyRelease>', self._on_target_repo_search)
# Refresh button
refresh_btn = ttk.Button(dropdown_frame, text="🔄", width=3,
command=self._refresh_target_repos)
refresh_btn.grid(row=0, column=1, padx=(0, 2))
# Search button
search_btn = ttk.Button(dropdown_frame, text="🔍", width=3,
command=self._search_target_repos)
search_btn.grid(row=0, column=2)
# Help text for target repo
help_label = ttk.Label(parent,
text=" ️ Upstream repo where PRs will be created. Type to search all GitHub repos.",
font=('Arial', 9), foreground='gray')
help_label.grid(row=row+1, column=0, columnspan=3, sticky=tk.W, padx=10)
# Start async loading of user's repos with edit access
self.dialog.after(100, self._load_target_repos_async)
def _create_forked_repo_dropdown(self, parent, row: int):
"""Create forked repository dropdown with local repo detection"""
ttk.Label(parent, text="Forked Repository:", font=('Arial', 10, 'bold')).grid(
@@ -354,10 +359,15 @@ class SettingsDialog:
self.entries['FORKED_REPO'] = self.forked_repo_var
# Refresh button
refresh_btn = ttk.Button(dropdown_frame, text="🔄", width=3,
refresh_btn = ttk.Button(dropdown_frame, text="🔄", width=3,
command=self._refresh_forked_repos)
refresh_btn.grid(row=0, column=1)
refresh_btn.grid(row=0, column=1, padx=(0, 2))
# Clone button
clone_btn = ttk.Button(dropdown_frame, text="📥", width=3,
command=self._clone_forked_repo)
clone_btn.grid(row=0, column=2)
# Help text for forked repo
help_label = ttk.Label(parent,
text=" ️ Your fork where changes will be made. Leave empty to auto-detect from document URL.",
@@ -404,7 +414,128 @@ class SettingsDialog:
except Exception as e:
print(f"Error refreshing local repos: {e}")
def _clone_forked_repo(self):
"""Clone the selected forked repository to the local repo path"""
# Get selected repository
selected_repo = self.forked_repo_var.get().strip()
# Validate selection
if not selected_repo:
messagebox.showwarning("No Repository Selected",
"Please select a repository to clone.")
return
# Check if it's a section header
if selected_repo.startswith('---'):
messagebox.showwarning("Invalid Selection",
"Please select a repository, not a section header.")
return
# Get local repo path
local_repo_path = self.config.get('LOCAL_REPO_PATH', '').strip()
if not local_repo_path:
messagebox.showwarning("Local Path Not Configured",
"Please configure the Local Repository Path in settings first.")
return
# Create directory if it doesn't exist
try:
os.makedirs(local_repo_path, exist_ok=True)
except Exception as e:
messagebox.showerror("Directory Error",
f"Could not create local repository directory:\n{str(e)}")
return
# Extract repo name (handle both "owner/repo" and URLs)
repo_name = selected_repo
if repo_name.startswith('http'):
# Extract from URL
parts = repo_name.rstrip('/').split('/')
if len(parts) >= 2:
repo_name = f"{parts[-2]}/{parts[-1]}"
else:
messagebox.showerror("Invalid Repository",
"Could not parse repository name from URL.")
return
# Validate format "owner/repo"
if '/' not in repo_name:
messagebox.showerror("Invalid Repository",
"Repository must be in 'owner/repo' format.")
return
# Extract just the repo name for the folder
folder_name = repo_name.split('/')[-1]
target_path = os.path.join(local_repo_path, folder_name)
# Check if directory already exists
if os.path.exists(target_path):
response = messagebox.askyesno("Directory Exists",
f"The directory '{folder_name}' already exists.\n\n"
f"Do you want to continue anyway?\n"
f"(This may fail if it's already a git repository)")
if not response:
return
# Construct clone URL
clone_url = f"https://github.com/{repo_name}.git"
# Clone in background thread
def clone_repo():
try:
# Run git clone
result = subprocess.run(
['git', 'clone', clone_url, target_path],
capture_output=True,
text=True,
timeout=300 # 5 minute timeout
)
# Update UI on main thread
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: self._handle_clone_result(result, repo_name, folder_name))
except subprocess.TimeoutExpired:
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: messagebox.showerror(
"Clone Timeout",
f"Cloning {repo_name} timed out after 5 minutes."))
except FileNotFoundError:
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: messagebox.showerror(
"Git Not Found",
"Git is not installed or not found in PATH.\n\n"
"Please install Git from: https://git-scm.com/downloads"))
except Exception as e:
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: messagebox.showerror(
"Clone Error",
f"An error occurred while cloning:\n{str(e)}"))
# Show progress message
messagebox.showinfo("Cloning Repository",
f"Cloning {repo_name} to:\n{target_path}\n\n"
f"This may take a few moments...")
# Start clone in background
thread = threading.Thread(target=clone_repo, daemon=True)
thread.start()
def _handle_clone_result(self, result, repo_name: str, folder_name: str):
"""Handle the result of a git clone operation"""
if result.returncode == 0:
messagebox.showinfo("Clone Successful",
f"Successfully cloned {repo_name}!\n\n"
f"Location: {folder_name}/")
# Refresh the dropdown to show the newly cloned repo
self._refresh_forked_repos()
else:
error_msg = result.stderr if result.stderr else result.stdout
messagebox.showerror("Clone Failed",
f"Failed to clone {repo_name}.\n\n"
f"Error:\n{error_msg}")
def _load_user_forks_async(self):
"""Load user's GitHub forks asynchronously"""
def load_forks():
@@ -412,18 +543,19 @@ class SettingsDialog:
github_token = self.config.get('GITHUB_PAT', '')
if not github_token:
return
from .github_api import GitHubGQL
github_api = GitHubGQL(github_token, dry_run=False)
self.forked_repos = github_api.get_user_forks()
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token)
repos = repo_fetcher.fetch_user_repos(repo_type='owner')
self.forked_repos = repo_fetcher.get_repo_names(repos)
# Update dropdown on main thread
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, self._update_forked_dropdown)
except Exception as e:
print(f"Error loading user forks: {e}")
threading.Thread(target=load_forks, daemon=True).start()
def _update_forked_dropdown(self):
@@ -436,22 +568,247 @@ class SettingsDialog:
return
current_values = list(self.forked_repo_dropdown['values'])
# Remove old GitHub forks section if exists
if '--- Your GitHub Forks ---' in current_values:
start_idx = current_values.index('--- Your GitHub Forks ---')
# Remove old GitHub repos section if exists
if '--- Your GitHub Repos ---' in current_values:
start_idx = current_values.index('--- Your GitHub Repos ---')
current_values = current_values[:start_idx]
# Add GitHub forks section
# Add GitHub repos section
if self.forked_repos:
current_values.append('--- Your GitHub Forks ---')
current_values.append('--- Your GitHub Repos ---')
current_values.extend(self.forked_repos)
self.forked_repo_dropdown['values'] = current_values
except Exception as e:
print(f"Error updating forked dropdown: {e}")
def _load_target_repos_async(self):
"""Load target repos (with push/admin access) asynchronously"""
def load_repos():
try:
github_token = self.config.get('GITHUB_PAT', '')
if not github_token:
return
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token)
repos = repo_fetcher.fetch_repos_with_permissions(min_permission='push')
self.target_repos = repo_fetcher.get_repo_names(repos)
# Update dropdown on main thread
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, self._update_target_dropdown)
except Exception as e:
print(f"Error loading target repos: {e}")
threading.Thread(target=load_repos, daemon=True).start()
def _update_target_dropdown(self):
"""Update the target repository dropdown"""
try:
if not hasattr(self, 'dialog') or not self.dialog.winfo_exists():
return
if not hasattr(self, 'target_repo_dropdown') or not self.target_repo_dropdown.winfo_exists():
return
current_values = [''] # Start with empty option
# Add user's repos with edit access
if self.target_repos:
current_values.append('--- Your Repos (with edit access) ---')
current_values.extend(self.target_repos)
self.target_repo_dropdown['values'] = current_values
except Exception as e:
print(f"Error updating target dropdown: {e}")
def _refresh_target_repos(self):
"""Refresh target repositories"""
self._load_target_repos_async()
def _search_target_repos(self):
"""Search for repositories on GitHub"""
query = self.target_repo_var.get().strip()
if not query:
return
def search_repos():
try:
github_token = self.config.get('GITHUB_PAT', '')
if not github_token:
return
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token)
repos = repo_fetcher.search_repositories(query, per_page=50)
search_results = repo_fetcher.get_repo_names(repos)
# Update dropdown on main thread
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: self._update_target_dropdown_with_search(search_results, query))
except Exception as e:
print(f"Error searching repos: {e}")
threading.Thread(target=search_repos, daemon=True).start()
def _update_target_dropdown_with_search(self, search_results, query):
"""Update target dropdown with search results"""
try:
if not hasattr(self, 'target_repo_dropdown') or not self.target_repo_dropdown.winfo_exists():
return
current_values = ['']
# Add user's repos
if self.target_repos:
current_values.append('--- Your Repos (with edit access) ---')
current_values.extend(self.target_repos)
# Add search results
if search_results:
current_values.append(f'--- Search Results for "{query}" ---')
current_values.extend(search_results)
self.target_repo_dropdown['values'] = current_values
except Exception as e:
print(f"Error updating target dropdown with search: {e}")
def _on_target_repo_search(self, _event):
"""Handle typing in target repo field for auto-search"""
# Debounce: only search after user stops typing for 500ms
if hasattr(self, '_search_timer'):
self.dialog.after_cancel(self._search_timer)
query = self.target_repo_var.get().strip()
if len(query) >= 3: # Only search if at least 3 characters
self._search_timer = self.dialog.after(500, self._search_target_repos)
def _create_ollama_model_dropdown(self, parent, row: int):
"""Create Ollama model dropdown with scan button"""
ttk.Label(parent, text="Ollama Model:", font=('Arial', 10, 'bold')).grid(
row=row, column=0, sticky=tk.W, pady=5, padx=10)
# Frame for dropdown and scan button
dropdown_frame = ttk.Frame(parent)
dropdown_frame.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=10)
dropdown_frame.columnconfigure(0, weight=1)
# Model dropdown
self.ollama_model_var = tk.StringVar(value=self.config.get('OLLAMA_MODEL', ''))
self.ollama_model_dropdown = ttk.Combobox(dropdown_frame, textvariable=self.ollama_model_var,
values=[''], width=47)
self.ollama_model_dropdown.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
self.entries['OLLAMA_MODEL'] = self.ollama_model_var
# Scan button
scan_btn = ttk.Button(dropdown_frame, text="🔍", width=3,
command=self._scan_ollama_models)
scan_btn.grid(row=0, column=1)
# Help text for Ollama model
help_label = ttk.Label(parent,
text=" ️ Click 🔍 to scan available models from your Ollama server.",
font=('Arial', 9), foreground='gray')
help_label.grid(row=row+1, column=0, columnspan=3, sticky=tk.W, padx=10)
def _scan_ollama_models(self):
"""Scan Ollama server for available models"""
ollama_url = self.entries.get('OLLAMA_URL').get().strip() if 'OLLAMA_URL' in self.entries else ''
if not ollama_url:
messagebox.showwarning("Ollama URL Required",
"Please enter the Ollama Server URL first.")
return
# Normalize URL
if not ollama_url.startswith('http'):
ollama_url = f"http://{ollama_url}"
# Scan in background thread
def scan_models():
try:
import requests
# Get API key if provided
ollama_api_key = self.entries.get('OLLAMA_API_KEY').get().strip() if 'OLLAMA_API_KEY' in self.entries else ''
headers = {}
if ollama_api_key:
headers['Authorization'] = f'Bearer {ollama_api_key}'
# Query Ollama API for models
response = requests.get(f"{ollama_url}/api/tags", headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
models = data.get('models', [])
model_names = [model.get('name', '') for model in models if model.get('name')]
# Update UI on main thread
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: self._update_ollama_models(model_names))
except requests.exceptions.ConnectionError:
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: messagebox.showerror(
"Connection Error",
f"Could not connect to Ollama server at:\n{ollama_url}\n\n"
f"Make sure Ollama is running and the URL is correct."))
except requests.exceptions.Timeout:
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: messagebox.showerror(
"Timeout",
f"Connection to Ollama server timed out."))
except requests.exceptions.HTTPError as e:
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
if e.response.status_code == 401:
self.dialog.after(0, lambda: messagebox.showerror(
"Authentication Error",
"Invalid API key. Please check your Ollama API Key."))
else:
self.dialog.after(0, lambda: messagebox.showerror(
"HTTP Error",
f"Error from Ollama server:\n{e}"))
except Exception as e:
if hasattr(self, 'dialog') and self.dialog.winfo_exists():
self.dialog.after(0, lambda: messagebox.showerror(
"Scan Error",
f"An error occurred while scanning for models:\n{str(e)}"))
# Start scan in background
threading.Thread(target=scan_models, daemon=True).start()
def _update_ollama_models(self, model_names):
"""Update the Ollama model dropdown with scanned models"""
if not model_names:
messagebox.showinfo("No Models Found",
"No models found on the Ollama server.\n\n"
"Use 'ollama pull <model>' to download models.")
return
try:
if hasattr(self, 'ollama_model_dropdown') and self.ollama_model_dropdown.winfo_exists():
current_value = self.ollama_model_var.get()
self.ollama_model_dropdown['values'] = model_names
# Keep current selection if it's still in the list
if current_value not in model_names and model_names:
self.ollama_model_var.set(model_names[0])
messagebox.showinfo("Models Found",
f"Found {len(model_names)} model(s):\n\n" +
"\n".join(f"{name}" for name in model_names[:10]) +
(f"\n\n...and {len(model_names) - 10} more" if len(model_names) > 10 else ""))
except Exception as e:
print(f"Error updating Ollama models: {e}")
def _create_dry_run_checkbox(self, parent, row: int):
"""Create dry run checkbox"""
self.dry_run_var = tk.BooleanVar()
@@ -502,22 +859,9 @@ class SettingsDialog:
results = []
# Test Azure DevOps
if config_values.get('AZURE_DEVOPS_QUERY') and config_values.get('AZURE_DEVOPS_PAT'):
try:
# Try to import and test Azure DevOps API
from .azure_devops_api import AzureDevOpsAPI
api = AzureDevOpsAPI(config_values.get('AZURE_DEVOPS_PAT'))
# Basic connection test (this would need actual implementation)
results.append("Azure DevOps: ✅ Configuration looks valid")
except ImportError:
results.append("Azure DevOps: ⚠️ Configuration set (API module not available)")
except Exception as e:
results.append(f"Azure DevOps: ❌ Error - {str(e)}")
elif config_values.get('AZURE_DEVOPS_QUERY') or config_values.get('AZURE_DEVOPS_PAT'):
results.append("Azure DevOps: ⚠️ Incomplete configuration")
# REMOVED: Azure DevOps test connection
# This was specific to Azure DevOps integration
# Test GitHub
if config_values.get('GITHUB_PAT'):
try:
@@ -605,15 +949,15 @@ class SettingsDialog:
config_values = self._get_config_values()
# Validate required fields
required_for_basic = ['AZURE_DEVOPS_QUERY', 'AZURE_DEVOPS_PAT', 'GITHUB_PAT']
required_for_basic = ['GITHUB_PAT']
missing_basic = [field for field in required_for_basic if not config_values.get(field)]
if missing_basic:
messagebox.showwarning(
"Missing Configuration",
f"The following required fields are missing:\n\n"
f"{', '.join(missing_basic)}\n\n"
f"These are required for basic functionality."
f"GitHub Personal Access Token is required for basic functionality."
)
return
@@ -686,7 +1030,7 @@ class SettingsDialog:
import os
# Create .env content
env_content = "# Azure DevOps to GitHub Tool Configuration\n"
env_content = "# GitHub Pulse Configuration\n"
env_content += "# Generated by Settings Dialog\n\n"
# Add all configuration values
@@ -822,11 +1166,11 @@ class SettingsDialog:
self.dialog.destroy()
def _clear_cache(self):
"""Clear all cached work items"""
"""Clear all cached items"""
result = messagebox.askyesno(
"Clear Cache",
"Are you sure you want to clear all cached items?\n\n"
"Cached work items and UUF items will be removed.\n"
"All cached data will be removed.\n"
"The next time you open the app, it will auto-load fresh data."
)
if result: