diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..4ba731c --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,151 @@ +name: Deploy Webzine + +on: + push: + branches: + - main + - dev + +jobs: + # ───────────────────────────────────────────── + # BUILD — commun aux deux branches + # ───────────────────────────────────────────── + build: + name: Build & Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Le tag d'image dépend de la branche : + # main → webzine:latest + # dev → webzine:dev + - name: Set image tag + id: vars + run: | + if [ "${{ gitea.ref_name }}" = "main" ]; then + echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT + echo "ENV_LABEL=production" >> $GITHUB_OUTPUT + else + echo "IMAGE_TAG=dev" >> $GITHUB_OUTPUT + echo "ENV_LABEL=development" >> $GITHUB_OUTPUT + fi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Connexion au registry Gitea intégré + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.REGISTRY_URL }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Webzine.WebApplication/Dockerfile + push: true + tags: ${{ vars.REGISTRY_URL }}/webzine/webzine:${{ steps.vars.outputs.IMAGE_TAG }} + cache-from: type=registry,ref=${{ vars.REGISTRY_URL }}/webzine/webzine:buildcache-${{ steps.vars.outputs.IMAGE_TAG }} + cache-to: type=registry,ref=${{ vars.REGISTRY_URL }}/webzine/webzine:buildcache-${{ steps.vars.outputs.IMAGE_TAG }},mode=max + + outputs: + image_tag: ${{ steps.vars.outputs.IMAGE_TAG }} + env_label: ${{ steps.vars.outputs.ENV_LABEL }} + + # ───────────────────────────────────────────── + # DEPLOY — Machine de PRODUCTION (branche main) + # ───────────────────────────────────────────── + deploy-production: + name: Deploy to Production + needs: build + if: gitea.ref_name == 'main' + runs-on: ubuntu-latest + + steps: + - name: Deploy via SSH to PRODUCTION server + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.PROD_SSH_HOST }} + username: ${{ secrets.PROD_SSH_USER }} + key: ${{ secrets.PROD_SSH_KEY }} + port: ${{ secrets.PROD_SSH_PORT || 22 }} + script: | + set -e + + echo "=== [PROD] Pulling image ===" + docker login ${{ vars.REGISTRY_URL }} \ + -u ${{ secrets.REGISTRY_USERNAME }} \ + -p ${{ secrets.REGISTRY_PASSWORD }} + + docker pull ${{ vars.REGISTRY_URL }}/webzine/webzine:latest + + echo "=== [PROD] Stopping old container ===" + docker stop webzine-prod 2>/dev/null || true + docker rm webzine-prod 2>/dev/null || true + + echo "=== [PROD] Starting new container ===" + docker run -d \ + --name webzine-prod \ + --restart unless-stopped \ + -p 80:8080 \ + -p 443:8081 \ + -v /opt/webzine/prod/data:/app/Data \ + -v /opt/webzine/prod/logs:/Logs \ + -e ASPNETCORE_ENVIRONMENT=Production \ + ${{ vars.REGISTRY_URL }}/webzine/webzine:latest + + echo "=== [PROD] Cleaning up old images ===" + docker image prune -f + + echo "=== [PROD] Deployment complete ===" + + # ───────────────────────────────────────────── + # DEPLOY — Machine de DÉVELOPPEMENT (branche dev) + # ───────────────────────────────────────────── + deploy-development: + name: Deploy to Development + needs: build + if: gitea.ref_name == 'dev' + runs-on: ubuntu-latest + + steps: + - name: Deploy via SSH to DEVELOPMENT server + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEV_SSH_HOST }} + username: ${{ secrets.DEV_SSH_USER }} + key: ${{ secrets.DEV_SSH_KEY }} + port: ${{ secrets.DEV_SSH_PORT || 22 }} + script: | + set -e + + echo "=== [DEV] Pulling image ===" + docker login ${{ vars.REGISTRY_URL }} \ + -u ${{ secrets.REGISTRY_USERNAME }} \ + -p ${{ secrets.REGISTRY_PASSWORD }} + + docker pull ${{ vars.REGISTRY_URL }}/webzine/webzine:dev + + echo "=== [DEV] Stopping old container ===" + docker stop webzine-dev 2>/dev/null || true + docker rm webzine-dev 2>/dev/null || true + + echo "=== [DEV] Starting new container ===" + docker run -d \ + --name webzine-dev \ + --restart unless-stopped \ + -p 8080:8080 \ + -v /opt/webzine/dev/data:/app/Data \ + -v /opt/webzine/dev/logs:/Logs \ + -e ASPNETCORE_ENVIRONMENT=Development \ + ${{ vars.REGISTRY_URL }}/webzine/webzine:dev + + echo "=== [DEV] Cleaning up old images ===" + docker image prune -f + + echo "=== [DEV] Deployment complete ===" \ No newline at end of file diff --git a/.gitea/workflows/pr-endpoint-check.yml b/.gitea/workflows/pr-endpoint-check.yml new file mode 100644 index 0000000..c0936d2 --- /dev/null +++ b/.gitea/workflows/pr-endpoint-check.yml @@ -0,0 +1,132 @@ +name: PR Endpoint Performance Check + +on: + pull_request: + branches: + - main + - master + - develop + +jobs: + endpoint-performance-check: + name: Test All Endpoints (< 1s) + runs-on: ubuntu-latest + + steps: + # ───────────────────────────────────────────── + # 1. Checkout code + # ───────────────────────────────────────────── + - name: Checkout PR branch + uses: actions/checkout@v4 + + # ───────────────────────────────────────────── + # 2. Setup .NET 10 + # ───────────────────────────────────────────── + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + # ───────────────────────────────────────────── + # 3. Restore & Build + # ───────────────────────────────────────────── + - name: Restore dependencies + run: dotnet restore Webzine.sln + + - name: Build solution + run: dotnet build Webzine.sln --no-restore --configuration Release + + # ───────────────────────────────────────────── + # 4. Run unit tests (entity tests) + # ───────────────────────────────────────────── + - name: Run unit tests + run: | + dotnet test Webzine.Entity.Tests/Webzine.Entity.Tests.csproj \ + --no-build \ + --configuration Release \ + --logger "console;verbosity=normal" + + # ───────────────────────────────────────────── + # 5. Start the web application in background + # ───────────────────────────────────────────── + - name: Start Webzine application + run: | + dotnet run \ + --project Webzine.WebApplication/Webzine.WebApplication.csproj \ + --configuration Release \ + --no-build \ + -- --urls "http://localhost:5038" & + + echo "Waiting for application to start..." + timeout 60 bash -c ' + until curl -sf http://localhost:5038 > /dev/null 2>&1; do + sleep 1 + done + ' + echo "Application is ready!" + + # ───────────────────────────────────────────── + # 6. Run endpoint performance tests + # ───────────────────────────────────────────── + - name: Test endpoint response times + id: perf_test + run: | + chmod +x scripts/test-endpoints.sh + bash scripts/test-endpoints.sh http://localhost:5038 1000 || true + FAIL_COUNT=$(grep -c "^\[FAIL\]\|^\[SLOW\]" /tmp/webzine_endpoint_report.txt || echo 0) + echo "failed=$FAIL_COUNT" >> "$GITHUB_OUTPUT" + + # ───────────────────────────────────────────── + # 7. Post report as PR comment + # ───────────────────────────────────────────── + - name: Post performance report as PR comment + if: always() + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_SERVER_URL: ${{ gitea.server_url }} + REPO: ${{ gitea.repository }} + PR_NUMBER: ${{ gitea.event.pull_request.number }} + run: | + REPORT_CONTENT=$(cat /tmp/webzine_endpoint_report.txt 2>/dev/null || echo "No report generated.") + FAILED_COUNT="${{ steps.perf_test.outputs.failed }}" + + if [ "${FAILED_COUNT:-0}" -gt 0 ]; then + HEADER="## ❌ Performance Check FAILED" + INTRO="${FAILED_COUNT} endpoint(s) exceeded 1 second or returned a server error." + else + HEADER="## ✅ Performance Check PASSED" + INTRO="All endpoints responded in under 1 second." + fi + + BODY=$(cat < Threshold: **1000ms** | Checked by the **PR Endpoint Performance** workflow. + EOF + ) + + curl -s -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg body "$BODY" '{body: $body}')" \ + "$GITEA_SERVER_URL/api/v1/repos/$REPO/issues/$PR_NUMBER/comments" + + # ───────────────────────────────────────────── + # 8. Fail the job if any endpoint failed + # ───────────────────────────────────────────── + - name: Enforce performance gate + run: | + FAILED="${{ steps.perf_test.outputs.failed }}" + if [ "${FAILED:-0}" -gt 0 ]; then + echo "❌ PR REJECTED: ${FAILED} endpoint(s) failed the 1-second threshold." + echo " Fix the slow/failing endpoints listed above before merging." + exit 1 + else + echo "✅ All endpoints passed the performance gate." + fi \ No newline at end of file diff --git a/scripts/test-endpoints.sh b/scripts/test-endpoints.sh new file mode 100644 index 0000000..a768ecd --- /dev/null +++ b/scripts/test-endpoints.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# ============================================================================= +# test-endpoints.sh +# Tests all Webzine endpoints and reports which ones exceed the threshold. +# +# Usage: +# ./scripts/test-endpoints.sh [BASE_URL] [MAX_MS] +# +# Examples: +# ./scripts/test-endpoints.sh # defaults: localhost:5038, 1000ms +# ./scripts/test-endpoints.sh http://localhost:5038 500 +# ============================================================================= + +set -euo pipefail + +BASE_URL="${1:-http://localhost:5038}" +MAX_MS="${2:-1000}" +REPORT_FILE="/tmp/webzine_endpoint_report.txt" +FAILED=0 +TOTAL=0 + +# ── Colours ────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +BOLD='\033[1m' +RESET='\033[0m' + +# ── Helpers ────────────────────────────────────────────────────────────────── +log() { echo -e "$*"; } +pass() { log "${GREEN}[PASS]${RESET} $*"; } +fail() { log "${RED}[FAIL]${RESET} $*"; FAILED=$((FAILED + 1)); } +slow() { log "${YELLOW}[SLOW]${RESET} $*"; FAILED=$((FAILED + 1)); } +info() { log "${CYAN}$*${RESET}"; } + +# ── check_endpoint ──────────────────────────────────────────────────────────── +# Arguments: +# $1 HTTP method (GET | POST) +# $2 URL (absolute) +# $3 Label (human-readable) +# $4 Body data (optional, for POST) +# $5 Content-Type (optional, default: application/x-www-form-urlencoded) +# ───────────────────────────────────────────────────────────────────────────── +check_endpoint() { + local METHOD="${1:-GET}" + local URL="$2" + local LABEL="$3" + local BODY="${4:-}" + local CONTENT_TYPE="${5:-application/x-www-form-urlencoded}" + + TOTAL=$((TOTAL + 1)) + + if [ "$METHOD" = "POST" ] && [ -n "$BODY" ]; then + RESPONSE=$(curl -s -o /dev/null \ + -w "%{http_code}|%{time_total}" \ + -X POST \ + -H "Content-Type: $CONTENT_TYPE" \ + -d "$BODY" \ + --max-time 10 \ + "$URL" 2>&1) || RESPONSE="000|9.999" + else + RESPONSE=$(curl -s -o /dev/null \ + -w "%{http_code}|%{time_total}" \ + -X GET \ + --max-time 10 \ + --location \ + "$URL" 2>&1) || RESPONSE="000|9.999" + fi + + HTTP_CODE=$(echo "$RESPONSE" | cut -d'|' -f1) + TIME_TOTAL=$(echo "$RESPONSE" | cut -d'|' -f2) + + # Convert to integer milliseconds — awk is locale-safe, no bc/printf decimal issues + TIME_MS=$(awk "BEGIN {printf \"%.0f\", $TIME_TOTAL * 1000}") + + # Evaluate result + if [ "${HTTP_CODE:-0}" -ge 500 ] 2>/dev/null; then + slow "$LABEL → HTTP $HTTP_CODE (${TIME_MS}ms)" + echo "[FAIL] $LABEL → HTTP $HTTP_CODE (${TIME_MS}ms)" >> "$REPORT_FILE" + elif [ "${TIME_MS:-99999}" -gt "$MAX_MS" ] 2>/dev/null; then + slow "$LABEL → ${TIME_MS}ms exceeds ${MAX_MS}ms threshold" + echo "[SLOW] $LABEL → ${TIME_MS}ms (limit: ${MAX_MS}ms)" >> "$REPORT_FILE" + else + pass "$LABEL → ${TIME_MS}ms" + echo "[OK] $LABEL → ${TIME_MS}ms" >> "$REPORT_FILE" + fi +} + +# ============================================================================= +# MAIN +# ============================================================================= + +# Initialise report +> "$REPORT_FILE" +cat >> "$REPORT_FILE" </dev/null || echo "$STYLE") + check_endpoint GET "$BASE_URL/titre/style/$ENCODED" "GET /titre/style/$STYLE" +done + +log "" +info "── Artiste ───────────────────────────────────────────────" +ARTISTES=("fatal-bazooka" "daft-punk" "justice" "kraftwerk") +for ARTISTE in "${ARTISTES[@]}"; do + check_endpoint GET "$BASE_URL/artiste/$ARTISTE" "GET /artiste/$ARTISTE" +done + +log "" +info "── Recherche (POST) ──────────────────────────────────────" +MOTS=("rock" "jazz" "pop" "metal") +for MOT in "${MOTS[@]}"; do + check_endpoint POST "$BASE_URL/recherche" \ + "POST /recherche (mot=$MOT)" \ + "mot=$MOT" +done + +log "" + +# ── ADMINISTRATION SECTION ──────────────────────────────────────────────────── +info "── Administration – Dashboard ────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Dashboard" "GET /Administration/Dashboard" + +log "" +info "── Administration – Artiste ──────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Artiste" "GET /Administration/Artiste (Index)" +check_endpoint GET "$BASE_URL/Administration/Artiste/Create" "GET /Administration/Artiste/Create" +check_endpoint GET "$BASE_URL/Administration/Artiste/Edit/1" "GET /Administration/Artiste/Edit/1" +check_endpoint GET "$BASE_URL/Administration/Artiste/Delete/1" "GET /Administration/Artiste/Delete/1" + +log "" +info "── Administration – Commentaire ──────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Commentaire" "GET /Administration/Commentaire (Index)" +check_endpoint GET "$BASE_URL/Administration/Commentaire/Delete/1" \ + "GET /Administration/Commentaire/Delete/1" + +log "" +info "── Administration – Style ────────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Style" "GET /Administration/Style (Index)" +check_endpoint GET "$BASE_URL/Administration/Style/Create" "GET /Administration/Style/Create" +check_endpoint GET "$BASE_URL/Administration/Style/Edit/1" "GET /Administration/Style/Edit/1" +check_endpoint GET "$BASE_URL/Administration/Style/Delete/1" "GET /Administration/Style/Delete/1" + +log "" +info "── Administration – Titre ────────────────────────────────" +check_endpoint GET "$BASE_URL/Administration/Titre" "GET /Administration/Titre (Index)" +check_endpoint GET "$BASE_URL/Administration/Titre/Create" "GET /Administration/Titre/Create" +check_endpoint GET "$BASE_URL/Administration/Titre/Edit/1" "GET /Administration/Titre/Edit/1" +check_endpoint GET "$BASE_URL/Administration/Titre/Delete/1" "GET /Administration/Titre/Delete/1" + +# ── SUMMARY ─────────────────────────────────────────────────────────────────── +PASSED=$((TOTAL - FAILED)) + +log "" +info "╔══════════════════════════════════════════════════════════╗" +info "║ Results ║" +info "╚══════════════════════════════════════════════════════════╝" +log " Total : ${TOTAL}" +log " ${GREEN}Passed${RESET} : ${PASSED}" + +if [ "$FAILED" -gt 0 ]; then + log " ${RED}Failed${RESET} : ${FAILED}" + log "" + log "${RED}${BOLD}❌ FAILED ENDPOINTS:${RESET}" + grep -E "^\[(FAIL|SLOW)\]" "$REPORT_FILE" | while IFS= read -r line; do + log " ${RED}→${RESET} $line" + done + log "" + log "${RED}PR should be rejected. Fix the endpoints above.${RESET}" +else + log " ${GREEN}Failed${RESET} : 0" + log "" + log "${GREEN}${BOLD}✅ All endpoints are within the ${MAX_MS}ms threshold.${RESET}" +fi + +log "" +log "Full report saved to: ${REPORT_FILE}" +log "" + +# Write summary to report +cat >> "$REPORT_FILE" <