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
17 changes: 16 additions & 1 deletion .github/workflows/template-inspection.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:
required: false
default: ""

permissions:
contents: read
issues: write
Copy link

Copilot AI Jul 11, 2025

Choose a reason for hiding this comment

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

The workflow grants write access to issues but does not interact with issues in this job. Remove or narrow the issues: write permission to follow the principle of least privilege.

Suggested change
issues: write

Copilot uses AI. Check for mistakes.

jobs:
inspect-templates:
runs-on: ubuntu-latest
Expand All @@ -29,9 +33,20 @@ jobs:
- name: Install dependencies
run: pdm install -G dev

- name: Set up Docker Compose
run: |
if ! command -v docker-compose &> /dev/null; then
echo "Installing Docker Compose..."
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
fi

docker --version
docker-compose --version

- name: Run template inspection
run: |
pdm run python scripts/inspect-templates.py --templates "${{ github.event.inputs.templates }}" --output template_inspection_results.json
pdm run python scripts/inspect-templates.py --templates "${{ github.event.inputs.templates }}" --output template_inspection_results.json --verbose

- name: Upload inspection results
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ htmlcov/
coverage.xml
.coverage.*
coverage/
temp/

.pdm-python
### VisualStudioCode template
Expand Down
149 changes: 128 additions & 21 deletions src/fastapi_fastkit/backend/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,24 @@ def _check_file_structure(self) -> bool:

for path in required_paths:
if not (self.template_path / path).exists():
self.errors.append(f"Missing required path: {path}")
error_msg = f"Missing required path: {path}"
self.errors.append(error_msg)
debug_log(f"File structure check failed: {error_msg}", "error")
return False

debug_log("File structure check passed", "info")
return True

def _check_file_extensions(self) -> bool:
"""Check all Python files have .py-tpl extension."""
for path in self.template_path.rglob("*"):
if path.is_file() and path.suffix == ".py":
self.errors.append(f"Found .py file instead of .py-tpl: {path}")
error_msg = f"Found .py file instead of .py-tpl: {path}"
self.errors.append(error_msg)
debug_log(f"File extension check failed: {error_msg}", "error")
return False

debug_log("File extension check passed", "info")
return True

def _check_dependencies(self) -> bool:
Expand All @@ -237,50 +245,73 @@ def _check_dependencies(self) -> bool:
setup_path = self.template_path / "setup.py-tpl"

if not req_path.exists():
self.errors.append("requirements.txt-tpl not found")
error_msg = "requirements.txt-tpl not found"
self.errors.append(error_msg)
debug_log(f"Dependencies check failed: {error_msg}", "error")
return False
if not setup_path.exists():
self.errors.append("setup.py-tpl not found")
error_msg = "setup.py-tpl not found"
self.errors.append(error_msg)
debug_log(f"Dependencies check failed: {error_msg}", "error")
return False

try:
with open(req_path, encoding="utf-8") as f:
deps = f.read().splitlines()
package_names = [dep.split("==")[0] for dep in deps if dep]
debug_log(f"Found dependencies: {package_names}", "debug")
if "fastapi" not in package_names:
self.errors.append(
"FastAPI dependency not found in requirements.txt-tpl"
)
error_msg = "FastAPI dependency not found in requirements.txt-tpl"
self.errors.append(error_msg)
debug_log(f"Dependencies check failed: {error_msg}", "error")
return False
except (OSError, UnicodeDecodeError) as e:
self.errors.append(f"Error reading requirements.txt-tpl: {e}")
error_msg = f"Error reading requirements.txt-tpl: {e}"
self.errors.append(error_msg)
debug_log(f"Dependencies check failed: {error_msg}", "error")
return False

debug_log("Dependencies check passed", "info")
return True

def _check_fastapi_implementation(self) -> bool:
"""Check if the template has a proper FastAPI server implementation."""
try:
core_modules = find_template_core_modules(self.temp_dir)
debug_log(f"Found core modules: {core_modules}", "debug")

if not core_modules["main"]:
self.errors.append("main.py not found in template")
error_msg = "main.py not found in template"
self.errors.append(error_msg)
debug_log(f"FastAPI implementation check failed: {error_msg}", "error")
return False

with open(core_modules["main"], encoding="utf-8") as f:
content = f.read()
if "FastAPI" not in content or "app" not in content:
self.errors.append("FastAPI app creation not found in main.py")
error_msg = "FastAPI app creation not found in main.py"
self.errors.append(error_msg)
debug_log(
f"FastAPI implementation check failed: {error_msg}", "error"
)
debug_log(f"main.py content preview: {content[:200]}...", "debug")
return False
except (OSError, UnicodeDecodeError) as e:
self.errors.append(f"Error checking FastAPI implementation: {e}")
error_msg = f"Error checking FastAPI implementation: {e}"
self.errors.append(error_msg)
debug_log(f"FastAPI implementation check failed: {error_msg}", "error")
return False

debug_log("FastAPI implementation check passed", "info")
return True

def _test_template(self) -> bool:
"""Run tests on the template using appropriate strategy based on configuration."""
test_dir = os.path.join(self.temp_dir, "tests")
if not os.path.exists(test_dir):
self.warnings.append("No tests directory found")
warning_msg = "No tests directory found"
self.warnings.append(warning_msg)
debug_log(f"Template warning: {warning_msg}", "warning")
return True

# Determine test strategy based on template configuration
Expand All @@ -295,9 +326,9 @@ def _test_with_docker_strategy(self) -> bool:

if not docker_available:
debug_log("Docker not available, trying fallback strategy", "warning")
self.warnings.append(
"Docker not available, using fallback testing strategy"
)
warning_msg = "Docker not available, using fallback testing strategy"
self.warnings.append(warning_msg)
debug_log(f"Template warning: {warning_msg}", "warning")
return self._test_with_fallback_strategy()

try:
Expand Down Expand Up @@ -374,7 +405,13 @@ def _test_with_standard_strategy(self) -> bool:
result = self._run_pytest_directly(venv_path)

if result.returncode != 0:
self.errors.append(f"Tests failed: {result.stderr}")
error_msg = f"Tests failed with return code {result.returncode}\n"
if result.stderr:
error_msg += f"STDERR:\n{result.stderr}\n"
if result.stdout:
error_msg += f"STDOUT:\n{result.stdout}\n"
self.errors.append(error_msg)
debug_log(f"Standard strategy tests failed: {error_msg}", "error")
return False

debug_log("All tests passed successfully", "info")
Expand Down Expand Up @@ -549,13 +586,23 @@ def _test_with_fallback_strategy(self) -> bool:
result = self._run_pytest_with_env(venv_path, env, fallback_config)

if result.returncode != 0:
self.errors.append(f"Fallback tests failed: {result.stderr}")
error_msg = (
f"Fallback tests failed with return code {result.returncode}\n"
)
if result.stderr:
error_msg += f"STDERR:\n{result.stderr}\n"
if result.stdout:
error_msg += f"STDOUT:\n{result.stdout}\n"
self.errors.append(error_msg)
debug_log(f"Fallback strategy tests failed: {error_msg}", "error")
return False

debug_log("Fallback tests passed successfully", "info")
self.warnings.append(
warning_msg = (
"Tests passed using fallback strategy (SQLite instead of PostgreSQL)"
)
self.warnings.append(warning_msg)
debug_log(f"Template warning: {warning_msg}", "warning")
return True

except Exception as e:
Expand Down Expand Up @@ -799,7 +846,15 @@ def _run_docker_tests(self, compose_file: str) -> bool:
)

if result.returncode != 0:
self.errors.append(f"Docker tests failed: {result.stderr}")
error_msg = (
f"Docker tests failed with return code {result.returncode}\n"
)
if result.stderr:
error_msg += f"STDERR:\n{result.stderr}\n"
if result.stdout:
error_msg += f"STDOUT:\n{result.stdout}\n"
self.errors.append(error_msg)
debug_log(f"Docker strategy tests failed: {error_msg}", "error")
return False

debug_log("Docker tests passed successfully", "info")
Expand Down Expand Up @@ -868,7 +923,15 @@ def _run_docker_exec_tests(self, compose_file: str) -> bool:
)

if result.returncode != 0:
self.errors.append(f"Docker exec tests failed: {result.stderr}")
error_msg = (
f"Docker exec tests failed with return code {result.returncode}\n"
)
if result.stderr:
error_msg += f"STDERR:\n{result.stderr}\n"
if result.stdout:
error_msg += f"STDOUT:\n{result.stdout}\n"
self.errors.append(error_msg)
debug_log(f"Docker exec strategy tests failed: {error_msg}", "error")
debug_log(f"Docker exec test stderr: {result.stderr}", "error")
debug_log(f"Docker exec test stdout: {result.stdout}", "info")
return False
Expand Down Expand Up @@ -904,11 +967,38 @@ def get_report(self) -> Dict[str, Any]:

:return: Dictionary containing inspection results
"""
is_valid = len(self.errors) == 0
template_name = self.template_path.name

# Log final inspection results
if is_valid:
debug_log(
f"Template inspection completed successfully for {template_name}",
"info",
)
if self.warnings:
debug_log(
f"Template {template_name} has {len(self.warnings)} warnings: {self.warnings}",
"warning",
)
else:
debug_log(
f"Template inspection failed for {template_name} with {len(self.errors)} errors",
"error",
)
for i, error in enumerate(self.errors, 1):
debug_log(f"Error {i}: {error}", "error")
if self.warnings:
debug_log(
f"Template {template_name} also has {len(self.warnings)} warnings: {self.warnings}",
"warning",
)

return {
"template_path": str(self.template_path),
"errors": self.errors,
"warnings": self.warnings,
"is_valid": len(self.errors) == 0,
"is_valid": is_valid,
}


Expand All @@ -919,21 +1009,38 @@ def inspect_fastapi_template(template_path: str) -> Dict[str, Any]:
:param template_path: Path to the template to inspect
:return: Inspection report dictionary
"""
template_name = Path(template_path).name
debug_log(
f"Starting template inspection for {template_name} at {template_path}", "info"
)

with TemplateInspector(template_path) as inspector:
is_valid = inspector.inspect_template()
report = inspector.get_report()

if is_valid:
print_success(f"Template {template_path} is valid!")
debug_log(
f"Template inspection completed successfully for {template_name}",
"info",
)
else:
print_error(f"Template {template_path} validation failed")
debug_log(f"Template inspection failed for {template_name}", "error")
for error in inspector.errors:
print_error(f" - {error}")

if inspector.warnings:
debug_log(
f"Template inspection for {template_name} has warnings", "warning"
)
for warning in inspector.warnings:
print_warning(f" - {warning}")

debug_log(
f"Template inspection completed for {template_name}. Valid: {is_valid}, Errors: {len(inspector.errors)}, Warnings: {len(inspector.warnings)}",
"info",
)
return report


Expand Down
Loading
Loading