diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a27357..fe7092b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,43 @@ -name: CI +name: CI — NexaCloud API on: push: - branches: [ "main" ] + branches: [main] pull_request: - branches: [ "main" ] + branches: [main] jobs: - build: + test: runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 - - name: Example step - run: echo "Add your build/test steps here!" + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Installer les dépendances + run: pip install -r ressources/requirements.txt + + - name: Lancement du test + run: pytest ressources/ -v + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Installer flake8 + run: pip install flake8 + + - name: "Lint avec flake8" + run: flake8 ressources/ --config ressources/.flake8 diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..d126532 --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,62 @@ +name: CI/CD — NexaCloud API + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + # ── Job 1 : Qualité ──────────────────────────────────────────────── + qualite: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - run: pip install -r ressources/requirements.txt + + - name: Lint + run: flake8 ressources/ --config ressources/.flake8 + + - name: Tests + run: pytest ressources/ -v --cov=ressources + + # ── Job 2 : Staging ─────────────────────────────────────────────── + staging: + runs-on: ubuntu-latest + needs: qualite # attend que le job qualite réussisse + environment: staging + if: github.ref_name == 'main' # uniquement sur la branche main + + steps: + - uses: actions/checkout@v4 + + - name: "Run Azure webapp deploy action using publish profile credentials" + uses: azure/webapps-deploy@v3 + with: + app-name: "DevAppAdrien" + publish-profile: ${{ secrets.azureWebAppPublishProfile }} + package: ressources/ + + # ── Job 3 : Production ──────────────────────────────────────────── + production: + + runs-on: ubuntu-latest + needs: staging + environment: production + if: github.ref_name == 'main' + + steps: + - uses: actions/checkout@v4 + + - name: "Run Azure webapp deploy action using publish profile credentials" + uses: azure/webapps-deploy@v3 + with: + app-name: "DevAppAdrien" + publish-profile: ${{ secrets.azureWebAppPublishProfile }} + package: ressources/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4405529..2065412 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,26 +1,32 @@ -name: Run Azure Login with OIDC +name: Deploy on: - workflow_dispatch: - -permissions: - id-token: write - contents: read + workflow_dispatch: jobs: - build-and-deploy: + deploy-staging: runs-on: ubuntu-latest + environment: staging + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Déployer en staging + run: | + echo "✅ Déploiement en staging réussi" + echo "URL : https://staging.nexacloud.example.com" + + deploy-production: + runs-on: ubuntu-latest + environment: production + needs: deploy-staging # attend que staging soit terminé ET approuvé + steps: - - name: Azure login - uses: azure/login@v3 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Checkout + uses: actions/checkout@v4 - - name: Azure CLI script - uses: azure/cli@v2 - with: - azcliversion: latest - inlineScript: | - az account show \ No newline at end of file + - name: Déployer en production + run: | + echo "🚀 Déploiement en production réussi" + echo "URL : https://nexacloud.example.com" diff --git a/.github/workflows/hello.yml b/.github/workflows/hello.yml new file mode 100644 index 0000000..37efbb3 --- /dev/null +++ b/.github/workflows/hello.yml @@ -0,0 +1,30 @@ +name: Hello NexaCloud + +on: + push: + branches: [main] + workflow_dispatch: # permet de déclencher manuellement depuis l'interface GitHub + +jobs: + salutation: + runs-on: ubuntu-latest + + steps: + - name: Checkout du code + uses: actions/checkout@v4 + + - name: Informations sur l'environnement + run: | + echo "Repo : ${{ github.repository }}" + echo "Branche : ${{ github.ref_name }}" + echo "Commit : ${{ github.sha }}" + echo "Acteur : ${{ github.actor }}" + + - name: Get date + run: echo "DATE=$(date +'%Y-%m-%d')" >> $GITHUB_ENV + + - name: Use date + run: echo ${{ env.DATE }} + + - name: Lister les fichiers du repo + run: ls -la diff --git a/.github/workflows/secrets.yml b/.github/workflows/secrets.yml new file mode 100644 index 0000000..a76250c --- /dev/null +++ b/.github/workflows/secrets.yml @@ -0,0 +1,19 @@ +name: Demo Secrets + +on: + workflow_dispatch: + +jobs: + demo: + runs-on: ubuntu-latest + + env: + API_KEY: ${{ secrets.API_KEY }} + + steps: + - name: Vérifier que le secret est défini + run: if [ -z "$API_KEY" ]; then + echo " Le secret API_KEY n'est pas défini" + exit 1 + fi + echo "✅ Le secret API_KEY est défini ( caractères)" diff --git a/README.md b/README.md index dfca308..f5ac2ad 100644 --- a/README.md +++ b/README.md @@ -198,10 +198,19 @@ Ouvrez le fichier `.github/workflows/ci.yml` déjà présent dans ce repo. Répondez aux questions suivantes **sans modifier le fichier** : 1. Sur quelle(s) branche(s) ce workflow se déclenche-t-il ? +# sur la branch main + 2. Combien de jobs contient-il ? + +# un job 3. Sur quel système d'exploitation tourne-t-il ? + +# Ubuntu 4. Quelle action installe Python ? + +# requirements.txt 5. Quelle commande lance les tests ? +# runs on Vérifiez vos réponses en allant dans l'onglet **Actions** de votre repo GitHub après votre premier push. @@ -534,9 +543,30 @@ C'est l'exercice le plus important de l'étape. 1. **Introduisez une erreur de style** dans `ressources/app.py` : ajoutez une ligne avec des espaces superflus en fin de ligne ou une ligne trop longue (> 100 caractères). Commitez et pushez. 2. **Observez** : quel job échoue ? Que dit le message d'erreur dans les logs ? + +Run flake8 ressources/ --config ressources/.flake8 +ressources/app.py:18:101: E501 line too long (123 > 100 characters) +Error: Process completed with exit code 1. + 3. **Corrigez** l'erreur, pushez à nouveau. 4. **Introduisez une erreur dans un test** : modifiez `test_app.py` pour qu'un assert soit faux (ex : `assert data["info"] == 999`). Commitez et pushez. 5. **Observez** : cette fois quel job échoue ? + +client = > + + def test_index_status(client): + """La route / retourne 200 avec le statut ok.""" + response = client.get("/") + assert response.status_code == 200 + data = response.get_json() +> assert data["info"] == 999 +E KeyError: 'info' + +ressources/test_app.py:21: KeyError +=========================== short test summary info ============================ +FAILED ressources/test_app.py::test_index_status - KeyError: 'info' +========================= 1 failed, 4 passed in 0.13s ========================== + 6. **Corrigez** et vérifiez que la CI repasse au vert. > 💡 Le but n'est pas de ne jamais casser la CI — c'est de savoir lire les logs et corriger rapidement. Cette compétence s'acquiert en cassant volontairement. diff --git a/azure.sh b/azure.sh new file mode 100644 index 0000000..8f46f5b --- /dev/null +++ b/azure.sh @@ -0,0 +1,28 @@ +# Dans votre terminal (Azure CLI doit être installé) + +RESOURCE_GROUP="asigurRG" +APP_NAME="DevAppAdrien" # nom unique obligatoire +LOCATION="francecentral" + +# Créer le resource group +az group create --name "$RESOURCE_GROUP" --location "$LOCATION" + +# Créer le plan App Service (B1 — nécessaire pour Always On et la stabilité) +az appservice plan create \ + --name "pythonappdev" \ + --resource-group "$RESOURCE_GROUP" \ + --sku B1 \ + --is-linux + +# Créer l'App Service Python +az webapp create \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --plan "plan-nexacloud" \ + --runtime "PYTHON:3.11" + +# Récupérer le publish profile (à coller dans les secrets GitHub) +az webapp deployment list-publishing-profiles \ + --name "$APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --xml diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..207097b --- /dev/null +++ b/notes.md @@ -0,0 +1,2 @@ + Mon TP Github Actions + Mon TP Github Actions diff --git a/ressources/app.py b/ressources/app.py index d7e75f2..5048b73 100644 --- a/ressources/app.py +++ b/ressources/app.py @@ -1,42 +1,48 @@ -""" -app.py — API NexaCloud -Mini API Flask utilisée comme cible du pipeline CI/CD dans le TP GitHub Actions. -""" - -from flask import Flask, jsonify - -app = Flask(__name__) - -# Simule un résumé de logs issu du TP Bash / PowerShell -LOG_SUMMARY = { - "info": 142, - "warning": 28, - "error": 12, - "critical": 3, -} - - -@app.route("/") -def index(): - return jsonify({"status": "ok", "service": "NexaCloud API", "version": "1.1.0"}) - - -@app.route("/health") -def health(): - return jsonify({"status": "healthy"}) - - -@app.route("/logs/summary") -def logs_summary(): - return jsonify(LOG_SUMMARY) - - -@app.route("/logs/critical") -def logs_critical(): - seuil = LOG_SUMMARY["critical"] - alerte = seuil > 0 - return jsonify({"critical_count": seuil, "alerte": alerte}) - - -if __name__ == "__main__": - app.run(debug=True, port=5001) +""" +app.py — API NexaCloud +Mini API Flask utilisée comme cible du pipeline CI/CD dans le TP GitHub Actions. +""" + +from flask import Flask, jsonify + +app = Flask(__name__) + +# Simule un résumé de logs issu du TP Bash / PowerShell +LOG_SUMMARY = { + "info": 142, + "warning": 28, + "error": 12, + "critical": 3, +} + + +@app.route("/") +def index(): + return jsonify({"status": "ok", "service": "NexaCloud API", "version": "1.1.0"}) + + +@app.route("/health") +def health(): + return jsonify({"status": "healthy"}) + + +@app.route("/logs/summary") +def logs_summary(): + return jsonify(LOG_SUMMARY) + + +@app.route("/logs/critical") +def logs_critical(): + seuil = LOG_SUMMARY["critical"] + alerte = seuil > 0 + return jsonify({"critical_count": seuil, "alerte": alerte}) + + +@app.route("/logs/stats") +def logs_stats(): + total = sum(LOG_SUMMARY.values()) + return jsonify({"total": total, "breakdown": LOG_SUMMARY}) + + +if __name__ == "__main__": + app.run(debug=True, port=5001) diff --git a/ressources/test_app.py b/ressources/test_app.py index ce56a43..b796c54 100644 --- a/ressources/test_app.py +++ b/ressources/test_app.py @@ -1,59 +1,69 @@ -""" -test_app.py — Tests unitaires de l'API NexaCloud -Lancés par pytest dans le pipeline CI. -""" - -import pytest -from app import app - - -@pytest.fixture -def client(): - app.testing = True - return app.test_client() - - -def test_index_status(client): - """La route / retourne 200 avec le statut ok.""" - response = client.get("/") - assert response.status_code == 200 - data = response.get_json() - assert data["status"] == "ok" - assert data["service"] == "NexaCloud API" - - -def test_health(client): - """La route /health retourne healthy.""" - response = client.get("/health") - assert response.status_code == 200 - assert response.get_json()["status"] == "healthy" - - -def test_logs_summary_structure(client): - """Le résumé de logs contient les 4 niveaux attendus.""" - response = client.get("/logs/summary") - assert response.status_code == 200 - data = response.get_json() - for niveau in ("info", "warning", "error", "critical"): - assert niveau in data - assert isinstance(data[niveau], int) - - -def test_logs_summary_values(client): - """Les compteurs de logs ont les valeurs attendues.""" - response = client.get("/logs/summary") - data = response.get_json() - assert data["info"] == 142 - assert data["warning"] == 28 - assert data["error"] == 12 - assert data["critical"] == 3 - - -def test_logs_critical_alerte(client): - """L'alerte est active quand il y a des critiques.""" - response = client.get("/logs/critical") - assert response.status_code == 200 - data = response.get_json() - assert "critical_count" in data - assert "alerte" in data - assert data["alerte"] is True +""" +test_app.py — Tests unitaires de l'API NexaCloud +Lancés par pytest dans le pipeline CI. +""" + +import pytest +from app import app + + +@pytest.fixture +def client(): + app.testing = True + return app.test_client() + + +def test_index_status(client): + """La route / retourne 200 avec le statut ok.""" + response = client.get("/") + assert response.status_code == 200 + data = response.get_json() + assert data["status"] == "ok" + assert data["service"] == "NexaCloud API" + + +def test_health(client): + """La route /health retourne healthy.""" + response = client.get("/health") + assert response.status_code == 200 + assert response.get_json()["status"] == "healthy" + + +def test_logs_summary_structure(client): + """Le résumé de logs contient les 4 niveaux attendus.""" + response = client.get("/logs/summary") + assert response.status_code == 200 + data = response.get_json() + for niveau in ("info", "warning", "error", "critical"): + assert niveau in data + assert isinstance(data[niveau], int) + + +def test_logs_summary_values(client): + """Les compteurs de logs ont les valeurs attendues.""" + response = client.get("/logs/summary") + data = response.get_json() + assert data["info"] == 142 + assert data["warning"] == 28 + assert data["error"] == 12 + assert data["critical"] == 3 + + +def test_logs_critical_alerte(client): + """L'alerte est active quand il y a des critiques.""" + response = client.get("/logs/critical") + assert response.status_code == 200 + data = response.get_json() + assert "critical_count" in data + assert "alerte" in data + assert data["alerte"] is True + + +def test_logs_stats(client): + """La route /logs/stats retourne le total et le détail.""" + response = client.get("/logs/stats") + assert response.status_code == 200 + data = response.get_json() + assert "total" in data + assert "breakdown" in data + assert data["total"] == 185