From f5fd487663af4303499e49d56dea4016a32ece95 Mon Sep 17 00:00:00 2001 From: naspirato Date: Wed, 26 Nov 2025 15:58:42 +0000 Subject: [PATCH 1/4] Add .cursorrules file for project documentation and enhance workflow routes with URL encoding filter - Introduced a new .cursorrules file containing comprehensive project documentation, including environment setup, project structure, and deployment instructions. - Added a URL encoding filter to Jinja2 templates to ensure proper encoding of query parameters in links. - Updated workflow routes to preserve 'ref' and 'inputs' in the 'Try again' and 'Run again' links on the result page. - Enhanced test coverage for the result page to verify preservation of 'ref' and 'inputs' in links. --- .cursorrules | 86 ++++++++++++++++++++++++++++++++ backend/routes/workflow.py | 30 ++++++++++-- frontend/templates/result.html | 4 +- tests/test_app.py | 90 ++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..c494307 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,86 @@ +# GitHub Action Executor - Cursor AI Rules + +## Project Overview +This is a FastAPI web application for triggering GitHub Actions workflows with OAuth authentication and permission checks. + +## Environment Setup +- **Virtual Environment**: The project uses a Python virtual environment located at `./venv/` +- **Activation**: Always activate venv before running commands: `source venv/bin/activate` (Linux/Mac) or `venv\Scripts\activate` (Windows) +- **Python Version**: Python 3.12 +- **Dependencies**: Install with `pip install -r requirements.txt` (after activating venv) + +## Project Structure +- `app.py` - Main FastAPI application entry point +- `backend/routes/` - API route handlers (auth, workflow, api) +- `backend/services/` - Business logic services (GitHub API, OAuth, permissions) +- `frontend/templates/` - Jinja2 HTML templates +- `frontend/static/` - Static files (CSS, images) +- `tests/` - Pytest test suite +- `config.py` - Application configuration + +## Running Commands +- **Start application (development)**: `python app.py` or `uvicorn app:app --host 0.0.0.0 --port 8000 --reload` +- **Start application (background with nohup)**: `./start.sh` (uses nohup, logs to nohup.out) +- **Stop application (nohup)**: `./stop.sh` (stops process started with start.sh) +- **Run as systemd service**: See "Deployment" section below +- **Run tests**: `pytest tests/ -v` (requires venv activation) +- **Run specific test**: `pytest tests/test_app.py::test_function_name -v` +- **Check syntax**: `python -m py_compile ` + +## Testing +- **Test location**: `tests/` directory +- **Test framework**: pytest with pytest-asyncio +- **Fixtures**: Defined in `tests/conftest.py` +- **Always activate venv before running tests**: `source venv/bin/activate && pytest tests/ -v` +- **Test client**: Uses FastAPI TestClient from `fastapi.testclient` + +## Code Style +- **Language**: Python 3.12 with type hints +- **Framework**: FastAPI (async/await) +- **Templates**: Jinja2 for HTML templates +- **Comments**: Use English for code comments and docstrings +- **Error handling**: Use try/except with proper logging + +## Key Dependencies +- FastAPI - Web framework +- httpx - Async HTTP client for GitHub API +- PyJWT - JWT token generation for GitHub App +- python-dotenv - Environment variable management +- pytest - Testing framework + +## Important Notes +- **Session Management**: Uses Starlette SessionMiddleware for OAuth state +- **GitHub App**: Requires GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and private key +- **OAuth**: Requires GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET +- **Environment Variables**: Loaded from `.env` file via python-dotenv +- **Templates**: Jinja2 templates in `frontend/templates/` with custom filters (urlencode) + +## Common Tasks +- **Adding new route**: Add to appropriate file in `backend/routes/` +- **Adding new service**: Add to `backend/services/` +- **Adding test**: Add to `tests/` directory, follow existing patterns +- **Modifying templates**: Edit files in `frontend/templates/` + + +## Deployment +- **Development mode**: Direct execution with `python app.py` or `uvicorn` with `--reload` +- **Production with nohup**: Use `./start.sh` script (logs to `nohup.out`, PID in `app.pid`) +- **Production with systemd**: Use `github-action-executor.service` file + - Service file location: `github-action-executor.service` + - Installation: `sudo cp github-action-executor.service /etc/systemd/system/` + - Commands: `sudo systemctl start/stop/restart/status github-action-executor` + - Enable on boot: `sudo systemctl enable github-action-executor` + - Logs: `sudo journalctl -u github-action-executor -f` + - **Important**: Edit service file to set correct paths (WorkingDirectory, PATH, User, EnvironmentFile) +- **Service configuration**: Service uses venv Python and loads `.env` file automatically +- **Process management**: When running as service, use systemctl commands, not direct process management + +## When Writing Code +- Always check if venv needs to be activated for Python commands +- Use async/await for all I/O operations (GitHub API calls) +- Add proper error handling and logging +- Write tests for new functionality +- Follow existing code patterns and structure +- Use type hints for function parameters and return values +- **When modifying startup logic**: Consider both development (direct) and production (service) modes + diff --git a/backend/routes/workflow.py b/backend/routes/workflow.py index 95bc582..10777f4 100644 --- a/backend/routes/workflow.py +++ b/backend/routes/workflow.py @@ -3,6 +3,7 @@ """ import os import logging +from urllib.parse import quote from fastapi import APIRouter, Request, HTTPException, Form, Query from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.templating import Jinja2Templates @@ -15,6 +16,13 @@ logger = logging.getLogger(__name__) router = APIRouter() templates = Jinja2Templates(directory="frontend/templates") +# Add urlencode filter to Jinja2 +def urlencode_filter(value): + """URL encode filter for Jinja2 templates""" + if value is None: + return "" + return quote(str(value), safe="") +templates.env.filters["urlencode"] = urlencode_filter async def _trigger_and_show_result( @@ -68,7 +76,9 @@ async def _trigger_and_show_result( "error": error_msg, "owner": owner, "repo": repo, - "workflow_id": workflow_id + "workflow_id": workflow_id, + "ref": ref, + "inputs": inputs } ) # Prevent caching @@ -152,7 +162,9 @@ async def _trigger_and_show_result( "error": str(e), "owner": owner, "repo": repo, - "workflow_id": workflow_id + "workflow_id": workflow_id, + "ref": ref, + "inputs": inputs } ) # Prevent caching @@ -224,7 +236,12 @@ async def trigger_workflow_get( "request": request, "user": request.session.get("user"), "success": False, - "error": error_msg + "error": error_msg, + "owner": owner or "", + "repo": repo or "", + "workflow_id": workflow_id or "", + "ref": ref or "", + "inputs": {} } ) # Prevent caching @@ -278,7 +295,12 @@ async def trigger_workflow_post( "request": request, "user": request.session.get("user"), "success": False, - "error": "Repository owner, name, and workflow_id are required" + "error": "Repository owner, name, and workflow_id are required", + "owner": owner or "", + "repo": repo or "", + "workflow_id": workflow_id or "", + "ref": ref or "", + "inputs": {} } ) diff --git a/frontend/templates/result.html b/frontend/templates/result.html index 36f170d..3790535 100644 --- a/frontend/templates/result.html +++ b/frontend/templates/result.html @@ -93,7 +93,7 @@

Success

View Workflow {% endif %} - + Run again Back to home @@ -118,7 +118,7 @@

Error

+ {% if return_url %} + + {% endif %}
@@ -847,10 +850,16 @@

Run GitHub Action

const formData = new FormData(form); for (const [key, value] of formData.entries()) { // Пропускаем служебные поля - if (!['owner', 'repo', 'workflow_id', 'ref'].includes(key) && value) { + if (!['owner', 'repo', 'workflow_id', 'ref', 'return_url'].includes(key) && value) { params.append(key, value); } } + + // Добавляем return_url отдельно, если есть + const returnUrlInput = form.querySelector('input[name="return_url"]'); + if (returnUrlInput && returnUrlInput.value) { + params.append('return_url', returnUrlInput.value); + } } } diff --git a/frontend/templates/result.html b/frontend/templates/result.html index 3790535..f74a6c0 100644 --- a/frontend/templates/result.html +++ b/frontend/templates/result.html @@ -88,6 +88,11 @@

Success