Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions .github/workflows/template-inspection.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ jobs:
- name: Install dependencies
run: pdm install -G dev

- name: Install UV package manager
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: Verify UV installation
run: |
export PATH="$HOME/.cargo/bin:$PATH"
uv --version
which uv

- name: Set up Docker Compose
run: |
if ! command -v docker-compose &> /dev/null; then
Expand All @@ -46,6 +57,8 @@ jobs:

- name: Run template inspection
run: |
export PATH="$HOME/.cargo/bin:$PATH"
uv --version # Verify UV is available
pdm run python scripts/inspect-templates.py --templates "${{ github.event.inputs.templates }}" --output template_inspection_results.json --verbose

- name: Upload inspection results
Expand Down Expand Up @@ -109,8 +122,8 @@ jobs:
labels: ['bug', 'template-inspection', 'automated']
});

- name: Comment on Success
if: success()
- name: Report Results
if: always()
uses: actions/github-script@v7
with:
script: |
Expand All @@ -119,11 +132,45 @@ jobs:
try {
const results = JSON.parse(fs.readFileSync('template_inspection_results.json', 'utf8'));

console.log(`✅ Weekly template inspection completed successfully!`);
console.log(`📊 Results: ${results.passed_templates}/${results.total_templates} templates passed`);
console.log(`📄 Detailed results available in workflow artifacts`);
let summary = `## 📊 Template Inspection Summary\n\n`;
summary += `**Inspection Date:** ${results.inspection_date}\n`;
summary += `**Total Templates:** ${results.total_templates}\n`;
summary += `**✅ Passed:** ${results.passed_templates}\n`;
summary += `**❌ Failed:** ${results.failed_templates}\n\n`;

if (results.passed_templates > 0) {
summary += `### ✅ Passed Templates:\n`;
results.results.forEach(result => {
if (result.is_valid) {
summary += `- ${result.template_name}\n`;
}
});
summary += `\n`;
}

if (results.failed_templates > 0) {
summary += `### ❌ Failed Templates:\n`;
results.results.forEach(result => {
if (!result.is_valid) {
summary += `- ${result.template_name}`;
if (result.errors && result.errors.length > 0) {
summary += `: ${result.errors[0]}`;
}
summary += `\n`;
}
});
}

console.log(summary);

if (results.failed_templates === 0) {
console.log(`🎉 All templates passed inspection!`);
} else {
console.log(`⚠️ ${results.failed_templates} template(s) failed inspection`);
}

} catch (error) {
console.log(`✅ Weekly template inspection completed successfully!`);
console.log(`📊 Template inspection completed`);
console.log(`📄 Results available in workflow artifacts`);
console.log(`❌ Error reading results: ${error.message}`);
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v1.1.1 (2025-08-15)

### Improvements

- fix template inspection workflow & script
- fixing uv supportation compatibility
- for now, template inspection is running with `uv` package manager

## v1.1.0 (2025-08-08)

### Features
Expand Down
2 changes: 1 addition & 1 deletion src/fastapi_fastkit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.1.0"
__version__ = "1.1.1"

import os

Expand Down
88 changes: 86 additions & 2 deletions src/fastapi_fastkit/backend/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
from fastapi_fastkit.backend.main import (
create_venv,
find_template_core_modules,
inject_project_metadata,
install_dependencies,
install_dependencies_with_manager,
)
from fastapi_fastkit.backend.transducer import copy_and_convert_template
from fastapi_fastkit.core.settings import settings
Expand Down Expand Up @@ -61,6 +63,10 @@ def __enter__(self) -> "TemplateInspector":
try:
os.makedirs(self.temp_dir, exist_ok=True)
copy_and_convert_template(str(self.template_path), self.temp_dir)

# Inject dummy metadata for inspection
self._inject_dummy_metadata()

self._cleanup_needed = True
self.template_config = self._load_template_config()
debug_log(f"Created temporary directory at {self.temp_dir}", "debug")
Expand Down Expand Up @@ -112,6 +118,43 @@ def _load_template_config(self) -> Optional[Dict[str, Any]]:
debug_log(f"Failed to load template configuration: {e}", "warning")
return None

def _inject_dummy_metadata(self) -> None:
"""Inject dummy metadata for template inspection."""
try:
# Use dummy metadata for inspection
dummy_metadata = {
"project_name": "test-template",
"author": "Template Inspector",
"author_email": "[email protected]",
"description": "Test project for template inspection",
}

inject_project_metadata(
self.temp_dir,
dummy_metadata["project_name"],
dummy_metadata["author"],
dummy_metadata["author_email"],
dummy_metadata["description"],
)
debug_log("Injected dummy metadata for template inspection", "info")

except Exception as e:
debug_log(f"Failed to inject dummy metadata: {e}", "warning")
self.warnings.append(f"Failed to inject metadata: {str(e)}")

def _detect_package_manager(self) -> str:
"""Detect the appropriate package manager for the template."""
# Check for pyproject.toml (modern Python packaging)
if os.path.exists(os.path.join(self.temp_dir, "pyproject.toml")):
return "uv" # Use UV for pyproject.toml based projects

# Check for requirements.txt (traditional pip)
if os.path.exists(os.path.join(self.temp_dir, "requirements.txt")):
return "pip"

# Default to pip if no dependency file found
return "pip"

def _check_docker_available(self) -> bool:
"""Check if Docker and Docker Compose are available."""
try:
Expand Down Expand Up @@ -393,7 +436,21 @@ def _test_with_standard_strategy(self) -> bool:
try:
# Create virtual environment for testing
venv_path = create_venv(self.temp_dir)
install_dependencies(self.temp_dir, venv_path)

# Detect and use appropriate package manager
package_manager = self._detect_package_manager()
debug_log(f"Using package manager: {package_manager}", "info")

try:
install_dependencies_with_manager(
self.temp_dir, venv_path, package_manager
)
except Exception as dep_error:
# Capture detailed dependency installation error
error_msg = f"Failed to install dependencies: {str(dep_error)}"
debug_log(f"Dependency installation failed: {dep_error}", "error")
self.errors.append(error_msg)
return False

# Check if scripts/test.sh exists
test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
Expand Down Expand Up @@ -557,7 +614,21 @@ def _test_with_fallback_strategy(self) -> bool:
try:
# Create virtual environment for testing
venv_path = create_venv(self.temp_dir)
install_dependencies(self.temp_dir, venv_path)

# Detect and use appropriate package manager
package_manager = self._detect_package_manager()
debug_log(f"Using package manager: {package_manager}", "info")

try:
install_dependencies_with_manager(
self.temp_dir, venv_path, package_manager
)
except Exception as dep_error:
# Capture detailed dependency installation error
error_msg = f"Failed to install dependencies: {str(dep_error)}"
debug_log(f"Dependency installation failed: {dep_error}", "error")
self.errors.append(error_msg)
return False

# Set up fallback environment (e.g., SQLite database)
fallback_config = self.template_config["fallback_testing"]
Expand Down Expand Up @@ -683,6 +754,14 @@ def _wait_for_services_healthy(self, compose_file: str, timeout: int) -> None:
# Check if all services are running
all_running = True
for service in services:
# Ensure service is a dictionary before calling .get()
if not isinstance(service, dict):
debug_log(
f"Service info is not a dictionary: {service}",
"warning",
)
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service type validation logic is duplicated in two locations (_wait_for_services_healthy and _verify_services_running). This could lead to maintenance issues. Consider extracting this validation into a helper method like _validate_service_info(service) to avoid code duplication.

Suggested change
)
if not self._validate_service_info(service):

Copilot uses AI. Check for mistakes.
continue

if service.get("State") != "running":
all_running = False
debug_log(
Expand Down Expand Up @@ -738,6 +817,11 @@ def _verify_services_running(self, compose_file: str) -> bool:
app_running = False

for service in services:
# Ensure service is a dictionary before calling .get()
if not isinstance(service, dict):
debug_log(f"Service info is not a dictionary: {service}", "warning")
continue

service_name = service.get("Name", "")
service_state = service.get("State", "")

Expand Down
63 changes: 61 additions & 2 deletions src/fastapi_fastkit/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@
def find_template_core_modules(project_dir: str) -> Dict[str, str]:
"""
Find core module files in the template project structure.
Returns a dictionary with paths to main.py, setup.py, and config files.
Returns a dictionary with paths to main.py, setup.py, pyproject.toml and config files.

:param project_dir: Path to the project directory
:return: Dictionary with paths to core modules
"""
core_modules = {"main": "", "setup": "", "config": ""}
core_modules = {"main": "", "setup": "", "pyproject": "", "config": ""}
template_paths = settings.TEMPLATE_PATHS

# Find main.py
Expand All @@ -54,6 +54,13 @@ def find_template_core_modules(project_dir: str) -> Dict[str, str]:
core_modules["setup"] = full_path
break

# Find pyproject.toml
for pyproject_path in template_paths["pyproject"]:
full_path = os.path.join(project_dir, pyproject_path)
if os.path.exists(full_path):
core_modules["pyproject"] = full_path
break

# Find config file
config_info = template_paths["config"]
if isinstance(config_info, dict):
Expand Down Expand Up @@ -166,6 +173,13 @@ def inject_project_metadata(
author_email,
description,
)
_process_pyproject_file(
core_modules.get("pyproject", ""),
project_name,
author,
author_email,
description,
)
_process_config_file(core_modules.get("config", ""), project_name)

print_success("Project metadata injected successfully")
Expand Down Expand Up @@ -245,6 +259,51 @@ def _process_config_file(config_py: str, project_name: str) -> None:
raise BackendExceptions(f"Failed to process config file: {e}")


def _process_pyproject_file(
pyproject_toml: str,
project_name: str,
author: str,
author_email: str,
description: str,
) -> None:
"""
Process pyproject.toml file and inject metadata.

:param pyproject_toml: Path to pyproject.toml file
:param project_name: Project name
:param author: Author name
:param author_email: Author email
:param description: Project description
"""
if not pyproject_toml or not os.path.exists(pyproject_toml):
return

try:
with open(pyproject_toml, "r", encoding="utf-8") as f:
content = f.read()

# Replace placeholders
replacements = {
"<project_name>": project_name,
"<author>": author,
"<author_email>": author_email,
"<description>": description,
}

for placeholder, value in replacements.items():
content = content.replace(placeholder, value)

with open(pyproject_toml, "w", encoding="utf-8") as f:
f.write(content)

debug_log("Injected metadata into pyproject.toml", "info")
print_info("Injected metadata into pyproject.toml")

except (OSError, UnicodeDecodeError) as e:
debug_log(f"Error processing pyproject.toml: {e}", "error")
raise BackendExceptions(f"Failed to process pyproject.toml: {e}")


def create_venv_with_manager(project_dir: str, manager_type: str = "pip") -> str:
"""
Create a virtual environment using the specified package manager.
Expand Down
17 changes: 14 additions & 3 deletions src/fastapi_fastkit/backend/package_managers/uv_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,27 @@ def install_dependencies(self, venv_path: str) -> None:
print_error(f"pyproject.toml file not found at {pyproject_path}")
raise BackendExceptions("pyproject.toml file not found")

# Install dependencies using UV sync
# Install dependencies using UV sync (including dev dependencies)
cmd = ["uv", "sync", "--group", "dev"]
debug_log(
f"Running UV command: {' '.join(cmd)} in {self.project_dir}", "info"
)

with console.status("[bold green]Installing dependencies with UV..."):
subprocess.run(
["uv", "sync"],
result = subprocess.run(
cmd,
cwd=str(self.project_dir),
check=True,
capture_output=True,
text=True,
)

# Log UV output for debugging
if result.stdout:
debug_log(f"UV stdout: {result.stdout}", "debug")
if result.stderr:
debug_log(f"UV stderr: {result.stderr}", "debug")

debug_log("Dependencies installed successfully with UV", "info")
print_success("Dependencies installed successfully with UV")

Expand Down
3 changes: 3 additions & 0 deletions src/fastapi_fastkit/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class FastkitConfig:
"setup.py",
"src/setup.py",
],
"pyproject": [
"pyproject.toml",
],
"config": {
"files": ["settings.py", "config.py"],
"paths": [
Expand Down
Loading
Loading