diff --git a/.gitea/actionlint.yaml b/.gitea/actionlint.yaml new file mode 100644 index 0000000..39b02c7 --- /dev/null +++ b/.gitea/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - xpeditis-deploy diff --git a/.gitea/actions/security/action.yml b/.gitea/actions/security/action.yml new file mode 100644 index 0000000..556de4a --- /dev/null +++ b/.gitea/actions/security/action.yml @@ -0,0 +1,30 @@ +name: Security gate +description: Dependency, secrets, infrastructure and workflow checks for Gitea 1.22. +runs: + using: composite + steps: + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + - uses: ./.gitea/actions/setup-trivy + - name: Validate workflows and deployment checks + shell: bash + run: | + archive="$RUNNER_TEMP/actionlint.tar.gz" + curl --fail --silent --show-error --location --retry 3 --max-time 120 \ + https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz \ + --output "$archive" + echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $archive" | sha256sum --check --strict + tar -xzf "$archive" -C "$RUNNER_TEMP" actionlint + ACTIONLINT_BIN="$RUNNER_TEMP/actionlint" bash scripts/ci/validate-workflows.sh + - name: Audit dependencies, secrets and infrastructure + shell: bash + run: bash scripts/ci/security-audit.sh + - name: Save security reports on Gitea + if: always() + uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol + with: + name: security-reports + path: ${{ runner.temp }}/security-reports/*.json + retention-days: 7 + if-no-files-found: error diff --git a/.gitea/actions/setup-trivy/action.yml b/.gitea/actions/setup-trivy/action.yml new file mode 100644 index 0000000..d3aa475 --- /dev/null +++ b/.gitea/actions/setup-trivy/action.yml @@ -0,0 +1,16 @@ +name: Install verified Trivy +description: Install a pinned scanner with a checked SHA256, without elevated privileges. +runs: + using: composite + steps: + - shell: bash + run: | + set -euo pipefail + install_dir="$RUNNER_TEMP/trivy-bin" + mkdir -p "$install_dir" + curl --fail --silent --show-error --location --retry 3 --max-time 120 \ + https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-64bit.tar.gz \ + --output "$install_dir/trivy.tar.gz" + echo "2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a $install_dir/trivy.tar.gz" | sha256sum --check --strict + tar -xzf "$install_dir/trivy.tar.gz" -C "$install_dir" trivy + echo "$install_dir" >> "$GITHUB_PATH" diff --git a/.github/workflows/rollback.yml b/.gitea/manual/rollback.reference.yml similarity index 100% rename from .github/workflows/rollback.yml rename to .gitea/manual/rollback.reference.yml diff --git a/.github/workflows/cd-main.yml b/.gitea/workflows/cd-main.yml similarity index 52% rename from .github/workflows/cd-main.yml rename to .gitea/workflows/cd-main.yml index 4b57d54..bc67649 100644 --- a/.github/workflows/cd-main.yml +++ b/.gitea/workflows/cd-main.yml @@ -19,33 +19,30 @@ name: CD Production # # 3. Le déploiement passe par SSH, pas par l'API Kubernetes. # L'API k3s (6443) n'est ouverte qu'aux IP d'administration. Les runners -# GitHub n'ont pas d'IP fixe : le job ouvre le port 22 pour la seule IP du +# Gitea n'ont pas d'IP fixe : le job ouvre le port 22 pour la seule IP du # runner via un firewall Hetzner dédié, puis le referme systématiquement. # -# Secrets et variables : voir infra/prod/env/github-secrets.md +# Secrets, runners et limites Gitea : voir docs/CI-CD-SECURITY.md on: push: branches: [main] - workflow_dispatch: - inputs: - tag: - description: "SHA court à déployer (laisser vide = HEAD de main)" - required: false - -concurrency: - group: cd-production - cancel-in-progress: false - -permissions: - contents: read env: REGISTRY: rg.fr-par.scw.cloud/weworkstudio - NODE_VERSION: '20' + NODE_VERSION: '22' K8S_NAMESPACE: xpeditis-prod jobs: + security: + name: Security gate + runs-on: ubuntu-latest + steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: ./.gitea/actions/security + # ═══ 1. Qualité ══════════════════════════════════════════════════════════ backend-quality: name: Backend — Lint @@ -54,14 +51,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm run lint + - run: npm ci --legacy-peer-deps + - run: npm run lint -- --no-fix frontend-quality: name: Frontend — Lint & Type-check @@ -70,12 +67,12 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - run: npm run lint - run: npm run type-check @@ -88,14 +85,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm ci --legacy-peer-deps + - run: npm test -- --ci --runInBand frontend-tests: name: Frontend — Tests unitaires @@ -105,107 +102,68 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm test -- --ci --runInBand # ═══ 2. Vérification de la provenance ════════════════════════════════════ - # Si l'image preprod-SHA n'existe pas, c'est que ce commit n'est jamais passé - # par la chaîne de preprod. Le déploiement est alors bloqué net. + # Exige un pipeline preprod réussi ET un arbre Git identique au code testé ici. verify-image: name: Vérifier l'image de preprod runs-on: ubuntu-latest - needs: [backend-tests, frontend-tests] + needs: [security, backend-tests, frontend-tests] outputs: sha: ${{ steps.sha.outputs.short }} + backend_digest: ${{ steps.sha.outputs.backend_digest }} + log_exporter_digest: ${{ steps.sha.outputs.log_exporter_digest }} steps: - - name: SHA court + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false + - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: nologin + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Resolve validated preprod release id: sha - run: | - RAW="${{ github.event.inputs.tag }}" - [ -n "$RAW" ] || RAW="${{ github.sha }}" - echo "short=$(echo "$RAW" | cut -c1-7)" >> $GITHUB_OUTPUT - - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: nologin - password: ${{ secrets.REGISTRY_TOKEN }} - - - name: Image backend preprod-SHA présente - run: | - TAG="${{ env.REGISTRY }}/xpeditis-backend:preprod-${{ steps.sha.outputs.short }}" - docker buildx imagetools inspect "$TAG" || { - echo "::error::$TAG introuvable. Ce commit n'a pas été construit par la chaîne de preprod." - echo "Fusionnez d'abord sur preprod et attendez que le pipeline passe au vert." - exit 1 - } - - - name: Image log-exporter preprod-SHA présente - run: | - TAG="${{ env.REGISTRY }}/xpeditis-log-exporter:preprod-${{ steps.sha.outputs.short }}" - docker buildx imagetools inspect "$TAG" || { - echo "::error::$TAG introuvable." - exit 1 - } - - # ═══ 3a. Promotion du backend (aucun rebuild) ════════════════════════════ - promote-backend: - name: Promouvoir le backend - runs-on: ubuntu-latest - needs: verify-image - steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: nologin - password: ${{ secrets.REGISTRY_TOKEN }} - - name: preprod-SHA → prod-SHA - run: | - SHA="${{ needs.verify-image.outputs.sha }}" - # Opération au niveau du manifeste : aucune couche n'est retransférée, - # le condensat de l'image reste identique à celui validé en preprod. - docker buildx imagetools create \ - --tag ${{ env.REGISTRY }}/xpeditis-backend:prod-${SHA} \ - --tag ${{ env.REGISTRY }}/xpeditis-backend:latest \ - ${{ env.REGISTRY }}/xpeditis-backend:preprod-${SHA} - docker buildx imagetools create \ - --tag ${{ env.REGISTRY }}/xpeditis-log-exporter:prod-${SHA} \ - --tag ${{ env.REGISTRY }}/xpeditis-log-exporter:latest \ - ${{ env.REGISTRY }}/xpeditis-log-exporter:preprod-${SHA} + run: bash scripts/ci/resolve-release.sh # ═══ 3b. Reconstruction du frontend avec les URLs de production ══════════ build-frontend: name: Reconstruire le frontend (URLs de production) runs-on: ubuntu-latest needs: verify-image + outputs: + digest: ${{ steps.build.outputs.digest }} steps: - - uses: actions/checkout@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - # On construit EXACTEMENT le commit vérifié, pas HEAD. + persist-credentials: false + # Cet arbre Git est identique à celui de la release preprod vérifiée. ref: ${{ github.sha }} - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ${{ env.REGISTRY }} username: nologin password: ${{ secrets.REGISTRY_TOKEN }} - - uses: docker/build-push-action@v5 + - id: build + uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: ./apps/frontend file: ./apps/frontend/Dockerfile push: true platforms: linux/amd64 tags: | - ${{ env.REGISTRY }}/xpeditis-frontend:prod-${{ needs.verify-image.outputs.sha }} - ${{ env.REGISTRY }}/xpeditis-frontend:latest + ${{ env.REGISTRY }}/xpeditis-frontend:candidate-prod-${{ github.sha }}-${{ github.run_id }} cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache-prod cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache-prod,mode=max build-args: | @@ -214,9 +172,12 @@ jobs: - name: Contrôle — l'URL de preprod ne doit pas figurer dans le bundle run: | - IMAGE="${{ env.REGISTRY }}/xpeditis-frontend:prod-${{ needs.verify-image.outputs.sha }}" + IMAGE="${{ env.REGISTRY }}/xpeditis-frontend@${{ steps.build.outputs.digest }}" CID=$(docker create "$IMAGE") - docker cp "$CID:/app/.next" /tmp/next-check 2>/dev/null || true + trap 'docker rm "$CID" >/dev/null' EXIT + docker cp "$CID:/app/.next" /tmp/next-check + test -d /tmp/next-check + trap - EXIT docker rm "$CID" >/dev/null if grep -rq "api.preprod.xpeditis.com" /tmp/next-check 2>/dev/null; then echo "::error::L'URL de preprod est figée dans le bundle de production." @@ -225,25 +186,88 @@ jobs: fi echo "Aucune URL de preprod dans le bundle." + image-security: + name: Image security (${{ matrix.service }}, ${{ matrix.arch }}) + runs-on: ubuntu-latest + needs: [verify-image, build-frontend] + strategy: + fail-fast: false + matrix: + service: [backend, frontend, log-exporter] + arch: [amd64] + steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: ./.gitea/actions/setup-trivy + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: nologin + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Scan the exact image before deployment + env: + IMAGE: ${{ env.REGISTRY }}/xpeditis-${{ matrix.service }}@${{ matrix.service == 'frontend' && needs.build-frontend.outputs.digest || (matrix.service == 'backend' && needs.verify-image.outputs.backend_digest || needs.verify-image.outputs.log_exporter_digest) }} + PLATFORM: linux/${{ matrix.arch }} + run: | + trivy image --platform "$PLATFORM" --scanners vuln --severity HIGH,CRITICAL \ + --ignore-unfixed=false --exit-code 1 --timeout 15m --format json \ + --output "$RUNNER_TEMP/image-security.json" "$IMAGE" + - name: Save image report + if: always() + uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol + with: + name: image-security-${{ matrix.service }}-${{ matrix.arch }} + path: ${{ runner.temp }}/image-security.json + retention-days: 14 + if-no-files-found: error + # ═══ 4. Déploiement ══════════════════════════════════════════════════════ deploy: name: Déployer en production - runs-on: ubuntu-latest - needs: [verify-image, promote-backend, build-frontend] - # Environnement protégé : activez « Required reviewers » pour exiger une - # validation humaine avant toute mise en production. - environment: - name: production - url: https://app.xpeditis.com + runs-on: xpeditis-deploy + needs: [verify-image, build-frontend, image-security] + # Gitea 1.22 ignores environments: serialize on the dedicated deployment runner. steps: - - uses: actions/checkout@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: nologin + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Publish scanned production images + env: + IMAGE_SHA: ${{ needs.verify-image.outputs.sha }} + BACKEND_DIGEST: ${{ needs.verify-image.outputs.backend_digest }} + FRONTEND_DIGEST: ${{ needs.build-frontend.outputs.digest }} + LOG_EXPORTER_DIGEST: ${{ needs.verify-image.outputs.log_exporter_digest }} + run: | + for service in backend frontend log-exporter; do + case "$service" in + backend) digest="$BACKEND_DIGEST" ;; + frontend) digest="$FRONTEND_DIGEST" ;; + log-exporter) digest="$LOG_EXPORTER_DIGEST" ;; + esac + docker buildx imagetools create \ + --tag "$REGISTRY/xpeditis-$service:prod-$IMAGE_SHA" \ + --tag "$REGISTRY/xpeditis-$service:latest" \ + "$REGISTRY/xpeditis-$service@$digest" + done - name: Installer le client Hetzner run: | - curl -fsSL https://github.com/hetznercloud/cli/releases/download/v1.49.0/hcloud-linux-amd64.tar.gz \ - | tar -xz -C /tmp hcloud - sudo install -m 0755 /tmp/hcloud /usr/local/bin/hcloud - hcloud version + mkdir -p "$RUNNER_TEMP/hcloud-bin" + curl --fail --silent --show-error --location --retry 3 --max-time 120 \ + https://github.com/hetznercloud/cli/releases/download/v1.49.0/hcloud-linux-amd64.tar.gz \ + --output "$RUNNER_TEMP/hcloud.tar.gz" + echo "dc6e5b0e6eaf9ef2baa5473a3eb49a11e80e72cdf2a01fdf7b0af975410e79cc $RUNNER_TEMP/hcloud.tar.gz" | sha256sum --check --strict + tar -xzf "$RUNNER_TEMP/hcloud.tar.gz" -C "$RUNNER_TEMP/hcloud-bin" hcloud + echo "$RUNNER_TEMP/hcloud-bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/hcloud-bin/hcloud" version - name: Ouvrir le port 22 pour l'IP de ce runner env: @@ -257,26 +281,29 @@ jobs: "protocol": "tcp", "port": "22", "source_ips": ["${RUNNER_IP}/32"], - "description": "GitHub Actions run ${{ github.run_id }}" + "description": "Gitea Actions run ${{ github.run_id }}" }] JSON hcloud firewall replace-rules "${{ vars.HCLOUD_CICD_FIREWALL }}" --rules-file /tmp/fw-open.json - name: Préparer SSH + env: + DEPLOY_SSH_KEY: ${{ secrets.PROD_SSH_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.PROD_SSH_KNOWN_HOSTS }} run: | - mkdir -p ~/.ssh && chmod 700 ~/.ssh - echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 + mkdir -p "$RUNNER_TEMP/deploy-ssh" && chmod 700 "$RUNNER_TEMP/deploy-ssh" + printf '%s\n' "$DEPLOY_SSH_KEY" > "$RUNNER_TEMP/deploy-ssh/id_ed25519" + chmod 600 "$RUNNER_TEMP/deploy-ssh/id_ed25519" # Empreinte épinglée : un détournement DNS ou BGP ne peut pas # rediriger le déploiement vers une machine tierce. - echo "${{ secrets.PROD_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts - chmod 600 ~/.ssh/known_hosts + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$RUNNER_TEMP/deploy-ssh/known_hosts" + chmod 600 "$RUNNER_TEMP/deploy-ssh/known_hosts" - name: Synchroniser infra/prod sur le serveur run: | rsync -az --delete \ --exclude '.terraform' --exclude '*.tfstate*' --exclude '*.tfvars' \ - -e "ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519" \ + -e "ssh -o StrictHostKeyChecking=yes -o UserKnownHostsFile=\"$RUNNER_TEMP/deploy-ssh/known_hosts\" -i \"$RUNNER_TEMP/deploy-ssh/id_ed25519\"" \ infra/prod/ \ "${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }}:/opt/xpeditis/infra-prod/" @@ -284,20 +311,21 @@ jobs: id: deploy run: | SHA="${{ needs.verify-image.outputs.sha }}" - ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \ + ssh -o StrictHostKeyChecking=yes -o UserKnownHostsFile="$RUNNER_TEMP/deploy-ssh/known_hosts" -i "$RUNNER_TEMP/deploy-ssh/id_ed25519" \ "${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }}" \ "deploy prod-${SHA}" - name: Tests de fumée depuis l'extérieur + id: smoke env: PROD_API_URL: ${{ vars.PROD_API_URL }} PROD_APP_URL: ${{ vars.PROD_APP_URL }} run: bash infra/prod/scripts/smoke-test.sh - name: Retour arrière si le déploiement a échoué - if: failure() && steps.deploy.conclusion == 'failure' + if: failure() && (steps.deploy.conclusion == 'failure' || steps.smoke.conclusion == 'failure') run: | - ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \ + ssh -o StrictHostKeyChecking=yes -o UserKnownHostsFile="$RUNNER_TEMP/deploy-ssh/known_hosts" -i "$RUNNER_TEMP/deploy-ssh/id_ed25519" \ "${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }}" \ "rollback" || true @@ -314,7 +342,7 @@ jobs: - name: Effacer la clé SSH if: always() - run: shred -u ~/.ssh/id_ed25519 2>/dev/null || rm -f ~/.ssh/id_ed25519 + run: shred -u "$RUNNER_TEMP/deploy-ssh/id_ed25519" 2>/dev/null || rm -f "$RUNNER_TEMP/deploy-ssh/id_ed25519" # ═══ 5. Notifications ════════════════════════════════════════════════════ notify-success: @@ -341,7 +369,7 @@ jobs: notify-failure: name: Notifier l'échec runs-on: ubuntu-latest - needs: [backend-quality, frontend-quality, backend-tests, frontend-tests, verify-image, promote-backend, build-frontend, deploy] + needs: [security, backend-quality, frontend-quality, backend-tests, frontend-tests, verify-image, build-frontend, image-security, deploy] if: failure() steps: - run: | diff --git a/.github/workflows/cd-preprod.yml b/.gitea/workflows/cd-preprod.yml similarity index 51% rename from .github/workflows/cd-preprod.yml rename to .gitea/workflows/cd-preprod.yml index 9da16bf..1528556 100644 --- a/.github/workflows/cd-preprod.yml +++ b/.gitea/workflows/cd-preprod.yml @@ -17,15 +17,20 @@ on: push: branches: [preprod] -concurrency: - group: cd-preprod - cancel-in-progress: false - env: REGISTRY: rg.fr-par.scw.cloud/weworkstudio - NODE_VERSION: '20' + NODE_VERSION: '22' jobs: + security: + name: Security gate + runs-on: ubuntu-latest + steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: ./.gitea/actions/security + # ── 1. Lint ───────────────────────────────────────────────────────── backend-quality: name: Backend — Lint @@ -34,14 +39,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm run lint + - run: npm ci --legacy-peer-deps + - run: npm run lint -- --no-fix frontend-quality: name: Frontend — Lint & Type-check @@ -50,12 +55,12 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - run: npm run lint - run: npm run type-check @@ -69,14 +74,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm ci --legacy-peer-deps + - run: npm test -- --ci --runInBand frontend-tests: name: Frontend — Unit Tests @@ -86,14 +91,14 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm test -- --ci --runInBand # ── 3. Integration Tests ───────────────────────────────────────────── integration-tests: @@ -116,8 +121,6 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 10 - ports: - - 5432:5432 redis: image: redis:7-alpine options: >- @@ -125,61 +128,70 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 10 - ports: - - 6379:6379 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps + - run: npm ci --legacy-peer-deps - name: Run integration tests env: NODE_ENV: test - DATABASE_HOST: localhost + TEST_DB_HOST: postgres + TEST_DB_PORT: 5432 + TEST_DB_USER: xpeditis_test + TEST_DB_PASSWORD: xpeditis_test_password + TEST_DB_NAME: xpeditis_test + DATABASE_HOST: postgres DATABASE_PORT: 5432 DATABASE_USER: xpeditis_test DATABASE_PASSWORD: xpeditis_test_password DATABASE_NAME: xpeditis_test DATABASE_SYNCHRONIZE: 'false' - REDIS_HOST: localhost + REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: '' JWT_SECRET: test-secret-key-ci SMTP_HOST: localhost SMTP_PORT: 1025 SMTP_FROM: test@xpeditis.com - run: npm run test:integration -- --passWithNoTests + run: npm run test:integration -- --ci --runInBand # ── 4. Docker Build & Push ─────────────────────────────────────────── # Tags: preprod (latest for this env) + preprod-SHA (used by prod for exact promotion) build-backend: name: Build Backend runs-on: ubuntu-latest - needs: integration-tests + needs: [security, integration-tests] outputs: sha: ${{ steps.sha.outputs.short }} + digest: ${{ steps.build.outputs.digest }} steps: - - uses: actions/checkout@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Short SHA id: sha run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: https://github.com/docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + with: + platforms: arm64 + - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ${{ env.REGISTRY }} username: nologin password: ${{ secrets.REGISTRY_TOKEN }} - - uses: docker/build-push-action@v5 + - id: build + uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: ./apps/backend file: ./apps/backend/Dockerfile push: true tags: | - ${{ env.REGISTRY }}/xpeditis-backend:preprod ${{ env.REGISTRY }}/xpeditis-backend:preprod-${{ steps.sha.outputs.short }} cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-backend:buildcache cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-backend:buildcache,mode=max @@ -188,27 +200,33 @@ jobs: build-frontend: name: Build Frontend runs-on: ubuntu-latest - needs: integration-tests + needs: [security, integration-tests] outputs: sha: ${{ steps.sha.outputs.short }} + digest: ${{ steps.build.outputs.digest }} steps: - - uses: actions/checkout@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Short SHA id: sha run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: https://github.com/docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + with: + platforms: arm64 + - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ${{ env.REGISTRY }} username: nologin password: ${{ secrets.REGISTRY_TOKEN }} - - uses: docker/build-push-action@v5 + - id: build + uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: ./apps/frontend file: ./apps/frontend/Dockerfile push: true tags: | - ${{ env.REGISTRY }}/xpeditis-frontend:preprod ${{ env.REGISTRY }}/xpeditis-frontend:preprod-${{ steps.sha.outputs.short }} cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache,mode=max @@ -220,42 +238,108 @@ jobs: build-log-exporter: name: Build Log Exporter runs-on: ubuntu-latest - needs: integration-tests + needs: [security, integration-tests] outputs: sha: ${{ steps.sha.outputs.short }} + digest: ${{ steps.build.outputs.digest }} steps: - - uses: actions/checkout@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Short SHA id: sha run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: https://github.com/docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + with: + platforms: arm64 + - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ${{ env.REGISTRY }} username: nologin password: ${{ secrets.REGISTRY_TOKEN }} - - uses: docker/build-push-action@v5 + - id: build + uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: ./apps/log-exporter file: ./apps/log-exporter/Dockerfile push: true tags: | - ${{ env.REGISTRY }}/xpeditis-log-exporter:preprod ${{ env.REGISTRY }}/xpeditis-log-exporter:preprod-${{ steps.sha.outputs.short }} cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-log-exporter:buildcache cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-log-exporter:buildcache,mode=max platforms: linux/amd64,linux/arm64 + image-security: + name: Image security (${{ matrix.service }}, ${{ matrix.arch }}) + runs-on: ubuntu-latest + needs: [build-backend, build-frontend, build-log-exporter] + strategy: + fail-fast: false + matrix: + service: [backend, frontend, log-exporter] + arch: [amd64, arm64] + steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: ./.gitea/actions/setup-trivy + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: nologin + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Scan the exact image before deployment + env: + IMAGE: ${{ env.REGISTRY }}/xpeditis-${{ matrix.service }}@${{ needs[format('build-{0}', matrix.service)].outputs.digest }} + PLATFORM: linux/${{ matrix.arch }} + run: | + trivy image --platform "$PLATFORM" --scanners vuln --severity HIGH,CRITICAL \ + --ignore-unfixed=false --exit-code 1 --timeout 15m --format json \ + --output "$RUNNER_TEMP/image-security.json" "$IMAGE" + - name: Save image report + if: always() + uses: https://github.com/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3, Gitea artifact protocol + with: + name: image-security-${{ matrix.service }}-${{ matrix.arch }} + path: ${{ runner.temp }}/image-security.json + retention-days: 14 + if-no-files-found: error + # ── 5. Deploy via Portainer ────────────────────────────────────────── deploy: name: Deploy to Preprod - runs-on: ubuntu-latest - needs: [build-backend, build-frontend, build-log-exporter] - environment: preprod + runs-on: xpeditis-deploy + needs: [build-backend, build-frontend, build-log-exporter, image-security] steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: nologin + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Publish scanned preprod images + env: + BACKEND_DIGEST: ${{ needs.build-backend.outputs.digest }} + FRONTEND_DIGEST: ${{ needs.build-frontend.outputs.digest }} + LOG_EXPORTER_DIGEST: ${{ needs.build-log-exporter.outputs.digest }} + run: | + for service in backend frontend log-exporter; do + case "$service" in + backend) digest="$BACKEND_DIGEST" ;; + frontend) digest="$FRONTEND_DIGEST" ;; + log-exporter) digest="$LOG_EXPORTER_DIGEST" ;; + esac + docker buildx imagetools create \ + --tag "$REGISTRY/xpeditis-$service:preprod" \ + "$REGISTRY/xpeditis-$service@$digest" + done - name: Deploy backend run: | - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${{ secrets.PORTAINER_WEBHOOK_BACKEND }}") + HTTP_CODE=$(curl --connect-timeout 10 --max-time 30 -s -o /dev/null -w "%{http_code}" -X POST "${{ secrets.PORTAINER_WEBHOOK_BACKEND }}") echo "Portainer response: HTTP $HTTP_CODE" if [[ "$HTTP_CODE" != "2"* ]]; then echo "ERROR: Portainer webhook failed with HTTP $HTTP_CODE" @@ -266,7 +350,7 @@ jobs: run: sleep 20 - name: Deploy frontend run: | - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${{ secrets.PORTAINER_WEBHOOK_FRONTEND }}") + HTTP_CODE=$(curl --connect-timeout 10 --max-time 30 -s -o /dev/null -w "%{http_code}" -X POST "${{ secrets.PORTAINER_WEBHOOK_FRONTEND }}") echo "Portainer response: HTTP $HTTP_CODE" if [[ "$HTTP_CODE" != "2"* ]]; then echo "ERROR: Portainer webhook failed with HTTP $HTTP_CODE" @@ -274,6 +358,27 @@ jobs: fi echo "Frontend webhook triggered." + - name: Verify backend health + env: + BASE_URL: ${{ secrets.PREPROD_BACKEND_URL }} + run: bash scripts/ci/health-check.sh "${BASE_URL:?Missing PREPROD_BACKEND_URL}/api/v1/health" + - name: Verify frontend health + env: + BASE_URL: ${{ secrets.PREPROD_FRONTEND_URL }} + run: bash scripts/ci/health-check.sh "${BASE_URL:?Missing PREPROD_FRONTEND_URL}" + + - name: Mark successfully deployed preprod images + env: + BACKEND_DIGEST: ${{ needs.build-backend.outputs.digest }} + LOG_EXPORTER_DIGEST: ${{ needs.build-log-exporter.outputs.digest }} + run: | + docker buildx imagetools create \ + --tag "$REGISTRY/xpeditis-backend:validated-preprod-$GITHUB_SHA" \ + "$REGISTRY/xpeditis-backend@$BACKEND_DIGEST" + docker buildx imagetools create \ + --tag "$REGISTRY/xpeditis-log-exporter:validated-preprod-$GITHUB_SHA" \ + "$REGISTRY/xpeditis-log-exporter@$LOG_EXPORTER_DIGEST" + # ── Notifications ──────────────────────────────────────────────────── notify-success: name: Notify Success @@ -298,14 +403,14 @@ jobs: notify-failure: name: Notify Failure runs-on: ubuntu-latest - needs: [backend-quality, frontend-quality, backend-tests, frontend-tests, integration-tests, build-backend, build-frontend, deploy] + needs: [security, backend-quality, frontend-quality, backend-tests, frontend-tests, integration-tests, build-backend, build-frontend, build-log-exporter, image-security, deploy] if: failure() steps: - run: | curl -s -H "Content-Type: application/json" -d '{ "embeds": [{ "title": "❌ Preprod Pipeline Failed", - "description": "Preprod was NOT deployed.", + "description": "Pipeline en échec. Vérifiez les rapports et l état réel des services avant de relancer.", "color": 15158332, "fields": [ {"name": "Author", "value": "${{ github.actor }}", "inline": true}, diff --git a/.github/workflows/ci.yml b/.gitea/workflows/ci.yml similarity index 55% rename from .github/workflows/ci.yml rename to .gitea/workflows/ci.yml index cc49e49..af83516 100644 --- a/.github/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -6,14 +6,19 @@ on: pull_request: branches: [dev] -concurrency: - group: dev-ci-${{ github.ref }} - cancel-in-progress: true - env: - NODE_VERSION: '20' + NODE_VERSION: '22' jobs: + security: + name: Security gate + runs-on: ubuntu-latest + steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: ./.gitea/actions/security + backend-quality: name: Backend — Lint runs-on: ubuntu-latest @@ -21,14 +26,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm run lint + - run: npm ci --legacy-peer-deps + - run: npm run lint -- --no-fix frontend-quality: name: Frontend — Lint & Type-check @@ -37,12 +42,12 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - run: npm run lint - run: npm run type-check @@ -55,14 +60,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm ci --legacy-peer-deps + - run: npm test -- --ci --runInBand frontend-tests: name: Frontend — Unit Tests @@ -72,19 +77,19 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm test -- --ci --runInBand notify-failure: name: Notify Failure runs-on: ubuntu-latest - needs: [backend-quality, frontend-quality, backend-tests, frontend-tests] + needs: [security, backend-quality, frontend-quality, backend-tests, frontend-tests] if: failure() steps: - name: Discord diff --git a/.github/workflows/pr-checks.yml b/.gitea/workflows/pr-checks.yml similarity index 53% rename from .github/workflows/pr-checks.yml rename to .gitea/workflows/pr-checks.yml index f78397f..e0d6b59 100644 --- a/.github/workflows/pr-checks.yml +++ b/.gitea/workflows/pr-checks.yml @@ -2,20 +2,25 @@ name: PR Checks # Required status checks — configure these in branch protection rules. # PRs to preprod : lint + type-check + unit tests + integration tests -# PRs to main : lint + type-check + unit tests only +# PRs to main : same checks, including integration and security on: pull_request: branches: [preprod, main] -concurrency: - group: pr-${{ github.event.pull_request.number }} - cancel-in-progress: true - env: - NODE_VERSION: '20' + NODE_VERSION: '22' jobs: + security: + name: Security gate + runs-on: ubuntu-latest + steps: + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: ./.gitea/actions/security + backend-quality: name: Backend — Lint runs-on: ubuntu-latest @@ -23,14 +28,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm run lint + - run: npm ci --legacy-peer-deps + - run: npm run lint -- --no-fix frontend-quality: name: Frontend — Lint & Type-check @@ -39,12 +44,12 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - run: npm run lint - run: npm run type-check @@ -57,14 +62,14 @@ jobs: run: working-directory: apps/backend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm ci --legacy-peer-deps + - run: npm test -- --ci --runInBand frontend-tests: name: Frontend — Unit Tests @@ -74,22 +79,20 @@ jobs: run: working-directory: apps/frontend steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/frontend/package-lock.json - run: npm ci --legacy-peer-deps - - run: npm test -- --passWithNoTests + - run: npm test -- --ci --runInBand - # Integration tests — PRs to preprod only - # Code going to main was already integration-tested when it passed through preprod + # Integration tests validate the actual merge candidate for both branches. integration-tests: name: Backend — Integration Tests runs-on: ubuntu-latest needs: backend-tests - if: github.base_ref == 'preprod' defaults: run: working-directory: apps/backend @@ -106,8 +109,6 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 10 - ports: - - 5432:5432 redis: image: redis:7-alpine options: >- @@ -115,31 +116,34 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 10 - ports: - - 6379:6379 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json - - run: npm install --legacy-peer-deps + - run: npm ci --legacy-peer-deps - name: Run integration tests env: NODE_ENV: test - DATABASE_HOST: localhost + TEST_DB_HOST: postgres + TEST_DB_PORT: 5432 + TEST_DB_USER: xpeditis_test + TEST_DB_PASSWORD: xpeditis_test_password + TEST_DB_NAME: xpeditis_test + DATABASE_HOST: postgres DATABASE_PORT: 5432 DATABASE_USER: xpeditis_test DATABASE_PASSWORD: xpeditis_test_password DATABASE_NAME: xpeditis_test DATABASE_SYNCHRONIZE: 'false' - REDIS_HOST: localhost + REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: '' JWT_SECRET: test-secret-key-ci SMTP_HOST: localhost SMTP_PORT: 1025 SMTP_FROM: test@xpeditis.com - run: npm run test:integration -- --passWithNoTests + run: npm run test:integration -- --ci --runInBand diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile index 4f8aa7b..38c03aa 100644 --- a/apps/backend/Dockerfile +++ b/apps/backend/Dockerfile @@ -1,7 +1,7 @@ # =============================================== # Stage 1: Dependencies Installation # =============================================== -FROM node:20-alpine AS dependencies +FROM node:22-alpine AS dependencies # Install build dependencies RUN apk add --no-cache python3 make g++ libc6-compat @@ -19,7 +19,7 @@ RUN npm ci --legacy-peer-deps # =============================================== # Stage 2: Build Application # =============================================== -FROM node:20-alpine AS builder +FROM node:22-alpine AS builder WORKDIR /app @@ -38,7 +38,7 @@ RUN npm prune --production --legacy-peer-deps # =============================================== # Stage 3: Production Image # =============================================== -FROM node:20-alpine AS production +FROM node:22-alpine AS production # Install dumb-init for proper signal handling RUN apk add --no-cache dumb-init diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile index 6bfa4a6..0b5ae20 100644 --- a/apps/frontend/Dockerfile +++ b/apps/frontend/Dockerfile @@ -1,7 +1,7 @@ # =============================================== # Stage 1: Dependencies Installation # =============================================== -FROM node:20-alpine AS dependencies +FROM node:22-alpine AS dependencies # Install build dependencies RUN apk add --no-cache libc6-compat @@ -18,7 +18,7 @@ RUN npm ci --legacy-peer-deps # =============================================== # Stage 2: Build Application # =============================================== -FROM node:20-alpine AS builder +FROM node:22-alpine AS builder WORKDIR /app @@ -48,7 +48,7 @@ RUN npm run build # =============================================== # Stage 3: Production Image # =============================================== -FROM node:20-alpine AS production +FROM node:22-alpine AS production # Install dumb-init for proper signal handling RUN apk add --no-cache dumb-init curl diff --git a/apps/log-exporter/Dockerfile b/apps/log-exporter/Dockerfile index d56679e..f10efc1 100644 --- a/apps/log-exporter/Dockerfile +++ b/apps/log-exporter/Dockerfile @@ -1,9 +1,9 @@ -FROM node:20-alpine +FROM node:22-alpine WORKDIR /app -COPY package.json ./ -RUN npm install --omit=dev +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev COPY src/ ./src/ diff --git a/apps/log-exporter/package-lock.json b/apps/log-exporter/package-lock.json new file mode 100644 index 0000000..6c2a129 --- /dev/null +++ b/apps/log-exporter/package-lock.json @@ -0,0 +1,965 @@ +{ + "name": "xpeditis-log-exporter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xpeditis-log-exporter", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "json2csv": "^6.0.0-alpha.2", + "node-fetch": "^3.3.2" + } + }, + "node_modules/@streamparser/json": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.6.tgz", + "integrity": "sha512-vL9EVn/v+OhZ+Wcs6O4iKE9EUpwHUqHmCtNUMWjqp+6dr85+XPOSGTEsqYNq1Vn04uk9SWlOVmx9J48ggJVT2Q==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/json2csv": { + "version": "6.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-6.0.0-alpha.2.tgz", + "integrity": "sha512-nJ3oP6QxN8z69IT1HmrJdfVxhU1kLTBVgMfRnNZc37YEY+jZ4nU27rBGxT4vaqM/KUCavLRhntmTuBFqZLBUcA==", + "license": "MIT", + "dependencies": { + "@streamparser/json": "^0.0.6", + "commander": "^6.2.0", + "lodash.get": "^4.4.2" + }, + "bin": { + "json2csv": "bin/json2csv.js" + }, + "engines": { + "node": ">= 12", + "npm": ">= 6.13.0" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/docs/CI-CD-SECURITY.md b/docs/CI-CD-SECURITY.md new file mode 100644 index 0000000..57cd644 --- /dev/null +++ b/docs/CI-CD-SECURITY.md @@ -0,0 +1,161 @@ +# CI/CD sécurisée — Gitea 1.22.6 + +La version du serveur `https://gitea.ops.xpeditis.com` a été vérifiée via +`/api/v1/version` : **1.22.6**. Les pipelines ciblent Gitea Actions / act_runner, +avec des jobs exécutés dans des conteneurs Linux AMD64. + +## Workflows et contrôles + +Les quatre workflows actifs se trouvent dans `.gitea/workflows/`. Les anciennes +copies `.github/workflows/` sont déplacées pour éviter une double exécution. +L'action composite `.gitea/actions/security` est appelée directement par chaque +pipeline ; aucun workflow réutilisable GitHub ni client `gh` n'est nécessaire. + +| Pipeline | Déclenchement | Contrôles | +| --- | --- | --- | +| Dev CI | Push/PR vers `dev` | Lint, types, tests unitaires, sécurité | +| PR Checks | PR vers `preprod` ou `main` | Idem + tests d'intégration PostgreSQL/Redis | +| CD Preprod | Push vers `preprod` | Idem + build, scan AMD64/ARM64, déploiement, contrôles HTTP | +| CD Production | Push vers `main` | Qualité/tests/sécurité, provenance préprod, build frontend, scan AMD64, déploiement SSH, tests de fumée | + +Le job **Security gate** exécute : + +- `npm audit --package-lock-only --audit-level=high` sur la racine, le backend, + le frontend et le log-exporter, sans installer de dépendances ni exécuter leurs + scripts. Les dépendances de développement sont incluses. +- Trivy sur les fichiers suivis du commit : détection de secrets et de mauvaises + configurations Docker/Kubernetes/Terraform, seuil HIGH/CRITICAL. +- La validation statique des workflows et les tests des scripts de promotion/santé. + +Les images sont analysées par digest, avec HIGH/CRITICAL bloquants, y compris +lorsqu'aucun correctif n'est disponible. Les erreurs de scanner ou de registre +échouent aussi : aucune exclusion générale ni échec masqué n'est ajouté. +Les installations applicatives utilisent `npm ci`, le runtime est Node 22 et les +actions sont épinglées par SHA. Trivy, Actionlint et Hetzner CLI sont téléchargés +avec vérification SHA256. Le lint backend ne réécrit plus les fichiers. + +Les rapports sont joints aux exécutions **Gitea Actions** avec +`actions/upload-artifact` **v3**, compatible avec le protocole de cette version +Gitea. `security-reports` est conservé 7 jours, `image-security-*` 14 jours selon +la politique de rétention du serveur. Les valeurs et extraits de code des secrets +sont retirés du rapport téléchargeable. Les URLs GitHub servent uniquement à +récupérer les actions publiques épinglées, pas à exécuter les pipelines. + +Ces scans ne remplacent pas un audit du code métier ni un test d'intrusion ; la +recherche de secrets porte sur le commit courant, pas tout l'historique. + +## Promotion sans API GitHub + +Gitea 1.22.6 n'expose pas d'API de consultation des runs Actions dans son Swagger. +La provenance utilise donc le registre Scaleway existant : après scans, webhooks +et contrôles HTTP backend/frontend réussis, le job préprod publie des marqueurs +`validated-preprod-` pour le backend et le log-exporter, à partir des +digests construits et scannés. Le log-exporter est construit/scanné par cette +chaîne ; son déploiement n'a pas de webhook ni de contrôle HTTP dédié existant. + +La production accepte seulement un commit de main ou un de ses parents dont +l'arbre Git est identique, avec les deux marqueurs présents. Elle récupère leurs +digests, les rescane et les promeut sans reconstruire le backend/log-exporter. +Le frontend est reconstruit avec les URLs prod puis scanné par digest. Les tags +`prod-SHA` et `latest` ne sont publiés que dans le job de déploiement, après scans. + +Utiliser un merge classique ou fast-forward de préprod vers main ; un squash ou +rebase qui supprime cette provenance sera refusé. Il faut une première préprod +réussie avec ces nouveaux workflows avant de promouvoir en production. La preuve +repose sur les droits du registre : réserver l'écriture des marqueurs au compte CI +et ne pas les créer manuellement. Ce n'est pas une attestation cryptographique. + +Les images candidates peuvent rester dans le registre après un échec de scan, +sans être publiées sous les alias d'environnement par le pipeline. + +## Configuration nécessaire sur ton instance + +Gitea 1.22 ignore notamment `concurrency`, `permissions`, `environment`, les délais +YAML de jobs et `workflow_dispatch`. Ces paramètres ne sont donc pas présentés +comme des protections effectives dans les workflows adaptés. + +1. Activer Actions dans le dépôt et disposer d'un runner Docker Linux AMD64 avec + le label `ubuntu-latest`, Git, Bash, Python 3, curl, tar, timeout et les outils + Docker. Les builds multiarchitecture ont besoin du support QEMU/binfmt. + Les services d'intégration sont joints via `postgres` et `redis`, sans ports + hôte fixes ; un runner en mode host n'est pas la cible de ces workflows. +2. Enregistrer **un seul runner** avec le label **`xpeditis-deploy`**, une capacité + **1**, et une limite d'exécution adaptée (par exemple 1 h). Les deux jobs de + déploiement utilisent ce label : publication des alias, déploiement, santé et + fermeture du firewall restent dans le même job. Ne pas donner ce label à + plusieurs runners. Le runner doit être isolé des jobs de PR et disposer de + Docker, Git, curl, SSH et rsync. Sans ce runner, les déploiements restent en attente. +3. Protéger `dev`, `preprod`, `main` dans les réglages de branches Gitea : interdire + les push directs/force-push et exiger **Security gate**, les deux contrôles + qualité et les deux tests unitaires ; ajouter l'intégration pour preprod/main. + Choisir les noms exacts proposés après la première exécution. Les approbations + humaines doivent être imposées sur les PR, pas via `environment`. +4. Renseigner les secrets/variables dans **Settings → Actions** du dépôt Gitea : + registre, webhooks Portainer, URLs préprod, URLs de build et identifiants + SSH/Hetzner déjà référencés. Les secrets ne sont pas des secrets d'environnement + GitHub. Les clés SSH temporaires sont écrites dans le répertoire du job. +5. Les fonctions manuelles `workflow_dispatch` ne sont pas disponibles sur 1.22. + L'ancien workflow de rollback est conservé hors du dossier actif dans + `.gitea/manual/rollback.reference.yml` comme référence, sans prétendre qu'il est + exécutable sur cette version. Le rollback SSH automatique de production reste + actif en cas d'échec du déploiement ou des tests de fumée. Pour une intervention + manuelle, utiliser la procédure serveur existante ; ne pas ajouter un faux + bouton de lancement Gitea. + +Ces réglages serveur/runners n'ont pas été modifiés depuis cette session. Aucun +pipeline distant ni déploiement n'a été lancé. + +## Mises à jour de dépendances + +Dependabot a été remplacé par `renovate.json`. La configuration propose des PR +vers `dev` pour npm, Docker et les actions, sans fusion automatique. Elle n'installe +ni ne démarre un bot. Un service Renovate doit être configuré séparément avec +`RENOVATE_PLATFORM=gitea`, `RENOVATE_ENDPOINT=https://gitea.ops.xpeditis.com/api/v1`, +un compte bot et son token, et le dépôt `David/xpeditis2.0`. Conserver le token hors +du dépôt. Les hashes des outils téléchargés dans les scripts doivent être mis à +jour en même temps que leurs versions. + +## Alertes déjà constatées + +Audits npm des lockfiles au 22 septembre 2026 : + +| Projet | HIGH | CRITICAL | +| --- | ---: | ---: | +| Racine | 0 | 0 | +| Backend | 50 | 0 | +| Frontend | 13 | 1 | +| Log-exporter | 0 | 0 | + +Ce sont des entrées de dépendances signalées, pas nécessairement autant de CVE +distinctes. L'entrée critique frontend concerne `next` ; npm propose une migration +majeure. Aucune mise à jour applicative forcée n'a été faite pour masquer ces résultats. + +Le scan local des configurations a relevé 11 alertes Kubernetes HIGH : systèmes +de fichiers inscriptibles, droits de monitoring et accès hôte. Elles restent à +examiner et bloquantes. Certains accès peuvent être nécessaires au monitoring. + +Les secrets Stripe en dur de la stack préprod ont été remplacés par les variables +Portainer obligatoires `STRIPE_SECRET_KEY` et `STRIPE_WEBHOOK_SECRET`. Renseigner +ces variables avant redéploiement. Si les anciennes valeurs sont actives, les +révoquer/renouveler dans Stripe : elles restent dans l'historique Git. Le scan de +ces configurations ne signale plus ce secret après modification. + +## Validation + +Les 19 tests offline de promotion et santé passent : arbre différent, marqueurs +manquants, panne du registre, digest invalide, mauvaise branche, saisie hostile et +échec HTTP sont refusés. Les tests vérifient aussi que les échecs et délais des +audits restent bloquants et que le rapport masque les valeurs de secrets. Actionlint valide la syntaxe après normalisation des URLs +d'actions Gitea ; ce contrôle ne remplace pas une exécution avec act_runner. + +```bash +ACTIONLINT_BIN=/chemin/vers/actionlint bash scripts/ci/validate-workflows.sh +``` + +Les installations ont été vérifiées avec `npm ci --dry-run --offline +--ignore-scripts --legacy-peer-deps`. Les builds Docker, les tests applicatifs sous +Node 22, le transport réel des artefacts et les déploiements sont encore à valider +sur les runners de l'instance. + +Références : [différences Gitea 1.22](https://docs.gitea.com/1.22/usage/actions/comparison/), +[Renovate sur Gitea](https://docs.renovatebot.com/modules/platform/gitea/). diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..e3cb5de --- /dev/null +++ b/renovate.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "baseBranchPatterns": ["dev"], + "enabledManagers": ["npm", "dockerfile", "github-actions"], + "automerge": false, + "prConcurrentLimit": 5 +} diff --git a/scripts/ci/health-check.sh b/scripts/ci/health-check.sh new file mode 100644 index 0000000..a69ee49 --- /dev/null +++ b/scripts/ci/health-check.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +url="${1:?A health-check URL is required}" +[[ "$url" == https://* ]] || { echo 'Health check requires HTTPS'; exit 1; } +for attempt in {1..12}; do + status=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ + --connect-timeout 5 --max-time 10 "$url") || status=000 + echo "Health check attempt $attempt: HTTP $status" + if [[ "$status" == 200 ]]; then exit 0; fi + sleep 10 +done +echo '::error::Service did not become healthy after deployment.' +exit 1 diff --git a/scripts/ci/resolve-release.sh b/scripts/ci/resolve-release.sh new file mode 100644 index 0000000..8243114 --- /dev/null +++ b/scripts/ci/resolve-release.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Resolve a successfully validated preprod commit with the exact production tree. +set -euo pipefail +: "${GITHUB_SHA:?}" "${REGISTRY:?}" "${GITHUB_OUTPUT:?}" +[[ "${GITHUB_REF:-}" == refs/heads/main ]] || { echo '::error::Production must run from main.'; exit 1; } +production_tree=$(git rev-parse "$GITHUB_SHA^{tree}") +if [[ -n "${REQUESTED_SHA:-}" ]]; then + [[ "$REQUESTED_SHA" =~ ^[0-9a-fA-F]{7,40}$ ]] || { echo '::error::Invalid commit SHA.'; exit 1; } + candidates=$(git rev-parse --verify "$REQUESTED_SHA^{commit}") +else + # A normal merge creates a new SHA: its second parent is preprod. + candidates=$(git rev-list --no-walk "$GITHUB_SHA" $(git show -s --format=%P "$GITHUB_SHA")) +fi +for candidate in $candidates; do + git merge-base --is-ancestor "$candidate" "$GITHUB_SHA" || continue + [[ "$(git rev-parse "$candidate^{tree}")" == "$production_tree" ]] || continue + valid=true + digests=() + for service in backend log-exporter; do + # Only the preprod deployment job publishes these markers after health checks. + image="$REGISTRY/xpeditis-$service:validated-preprod-$candidate" + if ! manifest=$(docker buildx imagetools inspect "$image"); then + valid=false + break + fi + digest=$(awk '/^Digest:/ {print $2; exit}' <<< "$manifest") + if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo 'Invalid manifest digest returned by the registry.' >&2 + exit 1 + fi + digests+=("$digest") + done + if [[ "$valid" == true ]]; then + echo "short=${candidate:0:7}" >> "$GITHUB_OUTPUT" + echo "commit=$candidate" >> "$GITHUB_OUTPUT" + echo "backend_digest=${digests[0]}" >> "$GITHUB_OUTPUT" + echo "log_exporter_digest=${digests[1]}" >> "$GITHUB_OUTPUT" + echo "Validated preprod commit: $candidate (identical source tree to production)" + exit 0 + fi +done +echo '::error::No validated preprod image pair matches this production tree. Merge preprod without squash/rebase, or provide its validated SHA.' +exit 1 diff --git a/scripts/ci/security-audit.sh b/scripts/ci/security-audit.sh new file mode 100644 index 0000000..a750e03 --- /dev/null +++ b/scripts/ci/security-audit.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail +reports="${RUNNER_TEMP:?}/security-reports" +mkdir -p "$reports" +failed=0 +for project in root backend frontend log-exporter; do + directory=. + [[ "$project" == root ]] || directory="apps/$project" + if ! (cd "$directory" && timeout 10m npm audit --package-lock-only --audit-level=high --json) > "$reports/npm-audit-$project.json"; then + echo "Dependency audit failed: $project (see report)" + failed=1 + fi +done +source_dir=$(mktemp -d "$RUNNER_TEMP/security-source.XXXXXX") +git archive HEAD | tar -x -C "$source_dir" +if ! trivy fs --scanners secret,misconfig --severity HIGH,CRITICAL --exit-code 1 \ + --timeout 10m --format json --output "$RUNNER_TEMP/source-security-raw.json" "$source_dir"; then + failed=1 +fi +python3 - <<'PYTHON' +import json, os +from pathlib import Path +raw = Path(os.environ['RUNNER_TEMP']) / 'source-security-raw.json' +if not raw.exists(): + raise SystemExit('Source scanner produced no report') +report = json.loads(raw.read_text()) +for result in report.get('Results', []): + for secret in result.get('Secrets', []): + secret.pop('Match', None) + secret.pop('Code', None) +(raw.parent / 'security-reports' / 'source-security.json').write_text(json.dumps(report)) +PYTHON +exit "$failed" diff --git a/scripts/ci/test_release_checks.py b/scripts/ci/test_release_checks.py new file mode 100644 index 0000000..baae140 --- /dev/null +++ b/scripts/ci/test_release_checks.py @@ -0,0 +1,188 @@ +"""Offline behavioral checks for deployment safety scripts (stdlib only).""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +SCRIPTS = Path(__file__).resolve().parent + + +class ReleaseChecks(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.bin = self.root / 'bin' + self.bin.mkdir() + self.env = dict(os.environ, PATH=f'{self.bin}:{os.environ["PATH"]}', + REGISTRY='registry.example.invalid/test', GITHUB_REF='refs/heads/main', + GITHUB_OUTPUT=str(self.root / 'output'), REQUESTED_SHA='') + self.git('init', '-q', '-b', 'main') + self.git('config', 'user.email', 'ci@example.invalid') + self.git('config', 'user.name', 'CI Test') + self.commit('initial') + self.git('checkout', '-q', '-b', 'preprod') + self.commit('release') + self.preprod = self.git('rev-parse', 'HEAD') + self.git('checkout', '-q', 'main') + self.git('merge', '-q', '--no-ff', 'preprod', '-m', 'Promote') + self.env['GITHUB_SHA'] = self.git('rev-parse', 'HEAD') + self.env['VALIDATED_SHA'] = self.preprod + self.mock('docker', """for arg in "$@"; do + case "$arg" in + *:validated-preprod-$VALIDATED_SHA) printf 'Digest: sha256:%064d\\n' 0; exit 0 ;; + esac +done +exit 1 +""") + + def git(self, *args): + return subprocess.check_output(['git', *args], cwd=self.root, stderr=subprocess.PIPE, + text=True).strip() + + def commit(self, text): + (self.root / 'source').write_text(text) + self.git('add', 'source') + self.git('commit', '-q', '-m', text) + + def mock(self, name, body): + path = self.bin / name + path.write_text('#!/bin/sh\n' + body) + path.chmod(0o755) + + def resolve(self): + return subprocess.run(['bash', str(SCRIPTS / 'resolve-release.sh')], cwd=self.root, + env=self.env, capture_output=True, text=True) + + def test_normal_merge_promotes_second_parent(self): + result = self.resolve() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn(f'commit={self.preprod}', (self.root / 'output').read_text()) + + def test_same_sha_promotion(self): + self.env['GITHUB_SHA'] = self.preprod + self.assertEqual(self.resolve().returncode, 0) + + def test_explicit_short_sha(self): + self.env['REQUESTED_SHA'] = self.preprod[:7] + self.assertEqual(self.resolve().returncode, 0) + + def test_modified_production_tree_blocks_release(self): + self.commit('untested change') + self.env['GITHUB_SHA'] = self.git('rev-parse', 'HEAD') + self.env['REQUESTED_SHA'] = self.preprod + self.assertNotEqual(self.resolve().returncode, 0) + + def test_missing_preprod_success_blocks_release(self): + self.env['VALIDATED_SHA'] = '0' * 40 + self.assertNotEqual(self.resolve().returncode, 0) + + def test_registry_failure_blocks_release(self): + self.mock('docker', 'exit 2\n') + self.assertNotEqual(self.resolve().returncode, 0) + + def test_missing_exporter_marker_blocks_release(self): + self.mock('docker', """case "$*" in + *xpeditis-log-exporter*) exit 1 ;; + *) printf 'Digest: sha256:%064d\\n' 0 ;; +esac +""") + self.assertNotEqual(self.resolve().returncode, 0) + + def test_invalid_digest_blocks_release(self): + self.mock('docker', "printf 'Digest: invalid\\n'\n") + self.assertNotEqual(self.resolve().returncode, 0) + + def test_release_exports_pinned_digests(self): + self.assertEqual(self.resolve().returncode, 0) + outputs = (self.root / 'output').read_text() + self.assertIn('backend_digest=sha256:' + '0' * 64, outputs) + self.assertIn('log_exporter_digest=sha256:' + '0' * 64, outputs) + + def test_input_is_never_executed(self): + self.env['REQUESTED_SHA'] = '$(touch injected)' + self.assertNotEqual(self.resolve().returncode, 0) + self.assertFalse((self.root / 'injected').exists()) + + def test_other_branch_cannot_deploy(self): + self.env['GITHUB_REF'] = 'refs/heads/dev' + self.assertNotEqual(self.resolve().returncode, 0) + + def health(self, response, url='https://example.invalid/health'): + self.mock('curl', response) + self.mock('sleep', 'exit 0\n') + return subprocess.run(['bash', str(SCRIPTS / 'health-check.sh'), url], + env=self.env, capture_output=True, text=True) + + def test_healthy_service(self): + self.assertEqual(self.health('printf 200\n').returncode, 0) + + def test_unhealthy_service_blocks(self): + result = self.health('printf 503\n') + self.assertNotEqual(result.returncode, 0) + self.assertIn('attempt 12', result.stdout) + + def test_network_failure_blocks(self): + self.assertNotEqual(self.health('exit 7\n').returncode, 0) + + def test_missing_https_blocks(self): + self.assertNotEqual(self.health('printf 200\n', '/api/v1/health').returncode, 0) + + +class AuditChecks(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.bin = self.root / 'bin' + self.bin.mkdir() + for project in ['backend', 'frontend', 'log-exporter']: + (self.root / 'apps' / project).mkdir(parents=True) + self.env = dict(os.environ, PATH=f'{self.bin}:{os.environ["PATH"]}', + RUNNER_TEMP=str(self.root), AUDIT_EXIT='0', SCAN_EXIT='0') + self.mock('timeout', 'shift\nexec "$@"\n') + self.mock('npm', """printf '%s' '{\"metadata\":{}}' +exit \"$AUDIT_EXIT\" +""") + self.mock('git', 'tar -cf - -T /dev/null\n') + self.mock('trivy', """while [ "$#" -gt 0 ]; do + if [ "$1" = --output ]; then shift; output="$1"; fi + shift +done +printf '%s' '{"Results":[{"Secrets":[{"RuleID":"fixture","Match":"synthetic-value","Code":{"Lines":[]}}]}]}' > "$output" +exit "$SCAN_EXIT" +""") + + def mock(self, name, body): + path = self.bin / name + path.write_text('#!/bin/sh\n' + body) + path.chmod(0o755) + + def audit(self): + return subprocess.run(['bash', str(SCRIPTS / 'security-audit.sh')], + cwd=self.root, env=self.env, capture_output=True, text=True) + + def test_success_and_secret_redaction(self): + self.assertEqual(self.audit().returncode, 0) + report = (self.root / 'security-reports' / 'source-security.json').read_text() + self.assertNotIn('synthetic-value', report) + self.assertNotIn('Code', report) + self.assertIn('fixture', report) + + def test_dependency_failure_is_not_masked_by_successful_source_scan(self): + self.env['AUDIT_EXIT'] = '1' + self.assertNotEqual(self.audit().returncode, 0) + self.assertEqual(len(list((self.root / 'security-reports').glob('npm-audit-*.json'))), 4) + + def test_source_failure_is_not_masked_by_successful_dependency_audits(self): + self.env['SCAN_EXIT'] = '1' + self.assertNotEqual(self.audit().returncode, 0) + + def test_timeout_fails_closed(self): + self.env['AUDIT_EXIT'] = '124' + self.assertNotEqual(self.audit().returncode, 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/ci/validate-workflows.sh b/scripts/ci/validate-workflows.sh new file mode 100644 index 0000000..399d9bd --- /dev/null +++ b/scripts/ci/validate-workflows.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail +validator="${ACTIONLINT_BIN:-actionlint}" +# Actionlint parses GitHub syntax. Normalize only Gitea's absolute action URLs; +# this is static validation, not an act_runner execution test. +for workflow in .gitea/workflows/*.yml; do + sed 's#uses: https://github.com/#uses: #' "$workflow" | + "$validator" -config-file .gitea/actionlint.yaml -shellcheck='' -stdin-filename "$workflow" - +done +bash -n scripts/ci/*.sh +python3 scripts/ci/test_release_checks.py