From e6e700c2ee42651ae244f5bb2f20be5a6a661190 Mon Sep 17 00:00:00 2001 From: Norvicdev <101652762+Norvicdev@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:29:48 +0300 Subject: [PATCH] entire app update: moder view app with static page, forgejo ci/cd workflow --- .dockerignore | 18 ++++ .forgejo/workflows/deploy.yml | 171 ++++++++++++++++++++++++++++++++++ Dockerfile | 34 ++++--- app/__init__.py | 12 +++ app/routes.py | 27 ++++++ app/static/css/style.css | 124 ++++++++++++++++++++++++ app/static/js/main.js | 10 ++ app/templates/base.html | 31 ++++++ app/templates/index.html | 32 +++++++ config.py | 5 + main.py | 13 --- requirements.txt | 5 +- run.py | 6 ++ 13 files changed, 460 insertions(+), 28 deletions(-) create mode 100644 .dockerignore create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 app/__init__.py create mode 100644 app/routes.py create mode 100644 app/static/css/style.css create mode 100644 app/static/js/main.js create mode 100644 app/templates/base.html create mode 100644 app/templates/index.html create mode 100644 config.py delete mode 100644 main.py create mode 100644 run.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4b0a877 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +.venv/ +*.log +.git +.gitignore +README.md +.env +.vscode/ +.idea/ +*.db +*.sqlite +.DS_Store \ No newline at end of file diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..5b99e90 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,171 @@ +name: Build and Deploy Flask App + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + +env: + REGISTRY: ${{ secrets.REGISTRY_URL || 'forgejo.example.com' }} + IMAGE_NAME: ${{ secrets.IMAGE_NAME || 'flask-app' }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '22' }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.14' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest flake8 + + - name: Lint with flake8 + run: | + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test with pytest (опционально) + run: | + # Добавьте тесты если есть + echo "Tests passed!" + + build-and-push: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Get commit hash + id: vars + run: | + echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "BUILD_TIME=$(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT + echo "IMAGE_TAG=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.IMAGE_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.run_number }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + COMMIT_HASH=${{ steps.vars.outputs.COMMIT_HASH }} + BUILD_TIME=${{ steps.vars.outputs.BUILD_TIME }} + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add host to known_hosts + run: | + ssh-keyscan -p ${{ env.DEPLOY_PORT }} -H ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to VPS + run: | + ssh -p ${{ env.DEPLOY_PORT }} ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'DEPLOY_SCRIPT' + set -e + + # Переменные для деплоя + APP_NAME="flask-app" + REGISTRY="${{ env.REGISTRY }}" + IMAGE_NAME="${{ env.IMAGE_NAME }}" + CONTAINER_NAME="flask-app" + PORT=5000 + DEPLOY_DIR="/opt/flask-app" + + echo "🚀 Начинаем деплой на сервер..." + + # Создаем директорию для приложения если её нет + mkdir -p $DEPLOY_DIR + cd $DEPLOY_DIR + + # Загружаем последний образ + echo "📦 Загружаем Docker образ..." + docker pull $REGISTRY/$IMAGE_NAME:latest + + # Останавливаем и удаляем старый контейнер + echo "🔄 Перезапускаем контейнер..." + docker stop $CONTAINER_NAME 2>/dev/null || true + docker rm $CONTAINER_NAME 2>/dev/null || true + + # Запускаем новый контейнер + echo "🚀 Запускаем новый контейнер..." + docker run -d \ + --name $CONTAINER_NAME \ + --restart unless-stopped \ + -p $PORT:5000 \ + -e APP_VERSION="${{ github.run_number }}" \ + -e COMMIT_HASH="${{ github.sha }}" \ + -e FLASK_ENV=production \ + -e SECRET_KEY="${{ secrets.APP_SECRET_KEY }}" \ + $REGISTRY/$IMAGE_NAME:latest + + # Проверяем здоровье + sleep 5 + if curl -f http://localhost:$PORT/health; then + echo "✅ Деплой успешно завершен!" + echo "🌐 Приложение доступно на порту $PORT" + else + echo "❌ Ошибка: приложение не отвечает!" + docker logs $CONTAINER_NAME --tail 50 + exit 1 + fi + + # Очищаем старые образы + echo "🧹 Очищаем старые Docker образы..." + docker image prune -f + DEPLOY_SCRIPT + + - name: Notify about deployment + if: always() + run: | + if [ ${{ job.status }} == 'success' ]; then + echo "✅ Деплой прошел успешно!" + else + echo "❌ Деплой завершился с ошибкой!" + exit 1 + fi \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 831f799..862816e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,34 @@ -FROM python:3.12-slim - -# Установка curl для healthcheck -RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +# Многоступенчатая сборка для оптимизации +FROM python:3.14-slim as builder WORKDIR /app +# Устанавливаем зависимости COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt +# Финальный образ +FROM python:3.14-slim + +WORKDIR /app + +# Копируем зависимости из builder +COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +# Копируем код приложения COPY . . -# Создаем non-root пользователя -RUN groupadd -r appuser && useradd -r -g appuser appuser +# Создаем пользователя для запуска +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser +# Экспортируем порт EXPOSE 5000 -HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ - CMD curl -f http://localhost:5000/health || exit 1 +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:5000/health')" || exit 1 -# Вариант 1: Запуск через Python -CMD ["python", "-c", "from main import app; app.run(host='0.0.0.0', port=5000)"] - -# Вариант 2: Если используете gunicorn -# CMD ["gunicorn", "--bind", "0.0.0.0:5000", "main:app"] \ No newline at end of file +# Запускаем через gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "run:app"] \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..fda8dd8 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,12 @@ +from flask import Flask +import os + + +def create_app(): + app = Flask(__name__) + app.config.from_object('config.Config') + + from app.routes import main + app.register_blueprint(main) + + return app \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..3a482d7 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,27 @@ +from flask import Blueprint, render_template, jsonify +import os +import datetime + +main = Blueprint('main', __name__) + +@main.route('/') +def index(): + return render_template('index.html', + title='Flask CI/CD Demo', + version=os.environ.get('APP_VERSION', '1.0.0')) + +@main.route('/health') +def health(): + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.datetime.now().isoformat(), + 'environment': os.environ.get('FLASK_ENV', 'production') + }) + +@main.route('/api/info') +def info(): + return jsonify({ + 'app': 'Flask CI/CD Demo', + 'version': os.environ.get('APP_VERSION', '1.0.0'), + 'commit': os.environ.get('COMMIT_HASH', 'unknown') + }) \ No newline at end of file diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000..65a0a5e --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,124 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + flex-direction: column; +} + +nav { + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + padding: 1rem 2rem; +} + +.nav-container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.nav-container h1 { + color: #333; + font-size: 1.5rem; +} + +.nav-links a { + color: #555; + text-decoration: none; + margin-left: 2rem; + padding: 0.5rem 1rem; + border-radius: 4px; + transition: all 0.3s; +} + +.nav-links a:hover { + background: #667eea; + color: white; +} + +main { + flex: 1; + max-width: 1200px; + margin: 2rem auto; + padding: 0 2rem; + width: 100%; +} + +.container { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 4px 20px rgba(0,0,0,0.1); +} + +h2 { + color: #333; + margin-bottom: 1.5rem; +} + +.status-card { + background: #f8f9fa; + border-radius: 8px; + padding: 1.5rem; + margin: 1.5rem 0; +} + +.status-item { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid #e9ecef; +} + +.status-item:last-child { + border-bottom: none; +} + +.label { + color: #666; + font-weight: 500; +} + +.value { + color: #333; + font-weight: 600; +} + +.green { + color: #28a745; +} + +.info-box { + background: #e8f4fd; + border-left: 4px solid #667eea; + padding: 1.5rem; + border-radius: 4px; + margin-top: 2rem; +} + +.info-box ol { + margin-left: 1.5rem; + margin-top: 0.5rem; +} + +.info-box li { + padding: 0.3rem 0; + color: #333; +} + +footer { + background: rgba(255, 255, 255, 0.9); + text-align: center; + padding: 1rem; + color: #666; + font-size: 0.9rem; +} \ No newline at end of file diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 100644 index 0000000..ddcf4bf --- /dev/null +++ b/app/static/js/main.js @@ -0,0 +1,10 @@ +document.addEventListener('DOMContentLoaded', function() { + console.log('Flask CI/CD App загружен!'); + + // Добавляем время сборки если есть + const footer = document.querySelector('footer p'); + if (footer) { + const buildTime = new Date().toLocaleString('ru-RU'); + footer.textContent += ` | Собрано: ${buildTime}`; + } +}); \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..456c32d --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,31 @@ + + +
+ + +