This commit is contained in:
David 2026-09-23 08:57:58 +02:00
parent 8d2193aaec
commit 9570316abf
19 changed files with 1881 additions and 268 deletions

3
.gitea/actionlint.yaml Normal file
View File

@ -0,0 +1,3 @@
self-hosted-runner:
labels:
- xpeditis-deploy

View File

@ -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

View File

@ -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"

View File

@ -19,33 +19,30 @@ name: CD Production
# #
# 3. Le déploiement passe par SSH, pas par l'API Kubernetes. # 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 # 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. # 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: on:
push: push:
branches: [main] 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: env:
REGISTRY: rg.fr-par.scw.cloud/weworkstudio REGISTRY: rg.fr-par.scw.cloud/weworkstudio
NODE_VERSION: '20' NODE_VERSION: '22'
K8S_NAMESPACE: xpeditis-prod K8S_NAMESPACE: xpeditis-prod
jobs: 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é ══════════════════════════════════════════════════════════ # ═══ 1. Qualité ══════════════════════════════════════════════════════════
backend-quality: backend-quality:
name: Backend — Lint name: Backend — Lint
@ -54,14 +51,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm run lint -- --no-fix
- run: npm install --legacy-peer-deps
- run: npm run lint
frontend-quality: frontend-quality:
name: Frontend — Lint & Type-check name: Frontend — Lint & Type-check
@ -70,12 +67,12 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm run lint - run: npm run lint
- run: npm run type-check - run: npm run type-check
@ -88,14 +85,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm test -- --ci --runInBand
- run: npm install --legacy-peer-deps
- run: npm test -- --passWithNoTests
frontend-tests: frontend-tests:
name: Frontend — Tests unitaires name: Frontend — Tests unitaires
@ -105,107 +102,68 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm test -- --passWithNoTests - run: npm test -- --ci --runInBand
# ═══ 2. Vérification de la provenance ════════════════════════════════════ # ═══ 2. Vérification de la provenance ════════════════════════════════════
# Si l'image preprod-SHA n'existe pas, c'est que ce commit n'est jamais passé # Exige un pipeline preprod réussi ET un arbre Git identique au code testé ici.
# par la chaîne de preprod. Le déploiement est alors bloqué net.
verify-image: verify-image:
name: Vérifier l'image de preprod name: Vérifier l'image de preprod
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests] needs: [security, backend-tests, frontend-tests]
outputs: outputs:
sha: ${{ steps.sha.outputs.short }} sha: ${{ steps.sha.outputs.short }}
backend_digest: ${{ steps.sha.outputs.backend_digest }}
log_exporter_digest: ${{ steps.sha.outputs.log_exporter_digest }}
steps: 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 id: sha
run: | run: bash scripts/ci/resolve-release.sh
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}
# ═══ 3b. Reconstruction du frontend avec les URLs de production ══════════ # ═══ 3b. Reconstruction du frontend avec les URLs de production ══════════
build-frontend: build-frontend:
name: Reconstruire le frontend (URLs de production) name: Reconstruire le frontend (URLs de production)
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: verify-image needs: verify-image
outputs:
digest: ${{ steps.build.outputs.digest }}
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: 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 }} ref: ${{ github.sha }}
- uses: docker/setup-buildx-action@v3 - uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@v3 - uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: nologin username: nologin
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- uses: docker/build-push-action@v5 - id: build
uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with: with:
context: ./apps/frontend context: ./apps/frontend
file: ./apps/frontend/Dockerfile file: ./apps/frontend/Dockerfile
push: true push: true
platforms: linux/amd64 platforms: linux/amd64
tags: | tags: |
${{ env.REGISTRY }}/xpeditis-frontend:prod-${{ needs.verify-image.outputs.sha }} ${{ env.REGISTRY }}/xpeditis-frontend:candidate-prod-${{ github.sha }}-${{ github.run_id }}
${{ env.REGISTRY }}/xpeditis-frontend:latest
cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache-prod cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache-prod
cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache-prod,mode=max cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache-prod,mode=max
build-args: | build-args: |
@ -214,9 +172,12 @@ jobs:
- name: Contrôle — l'URL de preprod ne doit pas figurer dans le bundle - name: Contrôle — l'URL de preprod ne doit pas figurer dans le bundle
run: | 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") 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 docker rm "$CID" >/dev/null
if grep -rq "api.preprod.xpeditis.com" /tmp/next-check 2>/dev/null; then 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." echo "::error::L'URL de preprod est figée dans le bundle de production."
@ -225,25 +186,88 @@ jobs:
fi fi
echo "Aucune URL de preprod dans le bundle." 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 ══════════════════════════════════════════════════════ # ═══ 4. Déploiement ══════════════════════════════════════════════════════
deploy: deploy:
name: Déployer en production name: Déployer en production
runs-on: ubuntu-latest runs-on: xpeditis-deploy
needs: [verify-image, promote-backend, build-frontend] needs: [verify-image, build-frontend, image-security]
# Environnement protégé : activez « Required reviewers » pour exiger une # Gitea 1.22 ignores environments: serialize on the dedicated deployment runner.
# validation humaine avant toute mise en production.
environment:
name: production
url: https://app.xpeditis.com
steps: 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 - name: Installer le client Hetzner
run: | run: |
curl -fsSL https://github.com/hetznercloud/cli/releases/download/v1.49.0/hcloud-linux-amd64.tar.gz \ mkdir -p "$RUNNER_TEMP/hcloud-bin"
| tar -xz -C /tmp hcloud curl --fail --silent --show-error --location --retry 3 --max-time 120 \
sudo install -m 0755 /tmp/hcloud /usr/local/bin/hcloud https://github.com/hetznercloud/cli/releases/download/v1.49.0/hcloud-linux-amd64.tar.gz \
hcloud version --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 - name: Ouvrir le port 22 pour l'IP de ce runner
env: env:
@ -257,26 +281,29 @@ jobs:
"protocol": "tcp", "protocol": "tcp",
"port": "22", "port": "22",
"source_ips": ["${RUNNER_IP}/32"], "source_ips": ["${RUNNER_IP}/32"],
"description": "GitHub Actions run ${{ github.run_id }}" "description": "Gitea Actions run ${{ github.run_id }}"
}] }]
JSON JSON
hcloud firewall replace-rules "${{ vars.HCLOUD_CICD_FIREWALL }}" --rules-file /tmp/fw-open.json hcloud firewall replace-rules "${{ vars.HCLOUD_CICD_FIREWALL }}" --rules-file /tmp/fw-open.json
- name: Préparer SSH - name: Préparer SSH
env:
DEPLOY_SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.PROD_SSH_KNOWN_HOSTS }}
run: | run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh mkdir -p "$RUNNER_TEMP/deploy-ssh" && chmod 700 "$RUNNER_TEMP/deploy-ssh"
echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519 printf '%s\n' "$DEPLOY_SSH_KEY" > "$RUNNER_TEMP/deploy-ssh/id_ed25519"
chmod 600 ~/.ssh/id_ed25519 chmod 600 "$RUNNER_TEMP/deploy-ssh/id_ed25519"
# Empreinte épinglée : un détournement DNS ou BGP ne peut pas # Empreinte épinglée : un détournement DNS ou BGP ne peut pas
# rediriger le déploiement vers une machine tierce. # rediriger le déploiement vers une machine tierce.
echo "${{ secrets.PROD_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$RUNNER_TEMP/deploy-ssh/known_hosts"
chmod 600 ~/.ssh/known_hosts chmod 600 "$RUNNER_TEMP/deploy-ssh/known_hosts"
- name: Synchroniser infra/prod sur le serveur - name: Synchroniser infra/prod sur le serveur
run: | run: |
rsync -az --delete \ rsync -az --delete \
--exclude '.terraform' --exclude '*.tfstate*' --exclude '*.tfvars' \ --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/ \ infra/prod/ \
"${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }}:/opt/xpeditis/infra-prod/" "${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }}:/opt/xpeditis/infra-prod/"
@ -284,20 +311,21 @@ jobs:
id: deploy id: deploy
run: | run: |
SHA="${{ needs.verify-image.outputs.sha }}" 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 }}" \ "${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }}" \
"deploy prod-${SHA}" "deploy prod-${SHA}"
- name: Tests de fumée depuis l'extérieur - name: Tests de fumée depuis l'extérieur
id: smoke
env: env:
PROD_API_URL: ${{ vars.PROD_API_URL }} PROD_API_URL: ${{ vars.PROD_API_URL }}
PROD_APP_URL: ${{ vars.PROD_APP_URL }} PROD_APP_URL: ${{ vars.PROD_APP_URL }}
run: bash infra/prod/scripts/smoke-test.sh run: bash infra/prod/scripts/smoke-test.sh
- name: Retour arrière si le déploiement a échoué - 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: | 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 }}" \ "${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }}" \
"rollback" || true "rollback" || true
@ -314,7 +342,7 @@ jobs:
- name: Effacer la clé SSH - name: Effacer la clé SSH
if: always() 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 ════════════════════════════════════════════════════ # ═══ 5. Notifications ════════════════════════════════════════════════════
notify-success: notify-success:
@ -341,7 +369,7 @@ jobs:
notify-failure: notify-failure:
name: Notifier l'échec name: Notifier l'échec
runs-on: ubuntu-latest 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() if: failure()
steps: steps:
- run: | - run: |

View File

@ -17,15 +17,20 @@ on:
push: push:
branches: [preprod] branches: [preprod]
concurrency:
group: cd-preprod
cancel-in-progress: false
env: env:
REGISTRY: rg.fr-par.scw.cloud/weworkstudio REGISTRY: rg.fr-par.scw.cloud/weworkstudio
NODE_VERSION: '20' NODE_VERSION: '22'
jobs: 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 ───────────────────────────────────────────────────────── # ── 1. Lint ─────────────────────────────────────────────────────────
backend-quality: backend-quality:
name: Backend — Lint name: Backend — Lint
@ -34,14 +39,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm run lint -- --no-fix
- run: npm install --legacy-peer-deps
- run: npm run lint
frontend-quality: frontend-quality:
name: Frontend — Lint & Type-check name: Frontend — Lint & Type-check
@ -50,12 +55,12 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm run lint - run: npm run lint
- run: npm run type-check - run: npm run type-check
@ -69,14 +74,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm test -- --ci --runInBand
- run: npm install --legacy-peer-deps
- run: npm test -- --passWithNoTests
frontend-tests: frontend-tests:
name: Frontend — Unit Tests name: Frontend — Unit Tests
@ -86,14 +91,14 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm test -- --passWithNoTests - run: npm test -- --ci --runInBand
# ── 3. Integration Tests ───────────────────────────────────────────── # ── 3. Integration Tests ─────────────────────────────────────────────
integration-tests: integration-tests:
@ -116,8 +121,6 @@ jobs:
--health-interval 5s --health-interval 5s
--health-timeout 5s --health-timeout 5s
--health-retries 10 --health-retries 10
ports:
- 5432:5432
redis: redis:
image: redis:7-alpine image: redis:7-alpine
options: >- options: >-
@ -125,61 +128,70 @@ jobs:
--health-interval 5s --health-interval 5s
--health-timeout 5s --health-timeout 5s
--health-retries 10 --health-retries 10
ports:
- 6379:6379
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json
- run: npm install --legacy-peer-deps
- name: Run integration tests - name: Run integration tests
env: env:
NODE_ENV: test 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_PORT: 5432
DATABASE_USER: xpeditis_test DATABASE_USER: xpeditis_test
DATABASE_PASSWORD: xpeditis_test_password DATABASE_PASSWORD: xpeditis_test_password
DATABASE_NAME: xpeditis_test DATABASE_NAME: xpeditis_test
DATABASE_SYNCHRONIZE: 'false' DATABASE_SYNCHRONIZE: 'false'
REDIS_HOST: localhost REDIS_HOST: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
REDIS_PASSWORD: '' REDIS_PASSWORD: ''
JWT_SECRET: test-secret-key-ci JWT_SECRET: test-secret-key-ci
SMTP_HOST: localhost SMTP_HOST: localhost
SMTP_PORT: 1025 SMTP_PORT: 1025
SMTP_FROM: test@xpeditis.com SMTP_FROM: test@xpeditis.com
run: npm run test:integration -- --passWithNoTests run: npm run test:integration -- --ci --runInBand
# ── 4. Docker Build & Push ─────────────────────────────────────────── # ── 4. Docker Build & Push ───────────────────────────────────────────
# Tags: preprod (latest for this env) + preprod-SHA (used by prod for exact promotion) # Tags: preprod (latest for this env) + preprod-SHA (used by prod for exact promotion)
build-backend: build-backend:
name: Build Backend name: Build Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: integration-tests needs: [security, integration-tests]
outputs: outputs:
sha: ${{ steps.sha.outputs.short }} sha: ${{ steps.sha.outputs.short }}
digest: ${{ steps.build.outputs.digest }}
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Short SHA - name: Short SHA
id: sha id: sha
run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- uses: docker/setup-buildx-action@v3 - uses: https://github.com/docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- uses: docker/login-action@v3 with:
platforms: arm64
- uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: nologin username: nologin
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- uses: docker/build-push-action@v5 - id: build
uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with: with:
context: ./apps/backend context: ./apps/backend
file: ./apps/backend/Dockerfile file: ./apps/backend/Dockerfile
push: true push: true
tags: | tags: |
${{ env.REGISTRY }}/xpeditis-backend:preprod
${{ env.REGISTRY }}/xpeditis-backend:preprod-${{ steps.sha.outputs.short }} ${{ env.REGISTRY }}/xpeditis-backend:preprod-${{ steps.sha.outputs.short }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-backend:buildcache cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-backend:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-backend:buildcache,mode=max cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-backend:buildcache,mode=max
@ -188,27 +200,33 @@ jobs:
build-frontend: build-frontend:
name: Build Frontend name: Build Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: integration-tests needs: [security, integration-tests]
outputs: outputs:
sha: ${{ steps.sha.outputs.short }} sha: ${{ steps.sha.outputs.short }}
digest: ${{ steps.build.outputs.digest }}
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Short SHA - name: Short SHA
id: sha id: sha
run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- uses: docker/setup-buildx-action@v3 - uses: https://github.com/docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- uses: docker/login-action@v3 with:
platforms: arm64
- uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: nologin username: nologin
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- uses: docker/build-push-action@v5 - id: build
uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with: with:
context: ./apps/frontend context: ./apps/frontend
file: ./apps/frontend/Dockerfile file: ./apps/frontend/Dockerfile
push: true push: true
tags: | tags: |
${{ env.REGISTRY }}/xpeditis-frontend:preprod
${{ env.REGISTRY }}/xpeditis-frontend:preprod-${{ steps.sha.outputs.short }} ${{ env.REGISTRY }}/xpeditis-frontend:preprod-${{ steps.sha.outputs.short }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache,mode=max cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-frontend:buildcache,mode=max
@ -220,42 +238,108 @@ jobs:
build-log-exporter: build-log-exporter:
name: Build Log Exporter name: Build Log Exporter
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: integration-tests needs: [security, integration-tests]
outputs: outputs:
sha: ${{ steps.sha.outputs.short }} sha: ${{ steps.sha.outputs.short }}
digest: ${{ steps.build.outputs.digest }}
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Short SHA - name: Short SHA
id: sha id: sha
run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- uses: docker/setup-buildx-action@v3 - uses: https://github.com/docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- uses: docker/login-action@v3 with:
platforms: arm64
- uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: nologin username: nologin
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- uses: docker/build-push-action@v5 - id: build
uses: https://github.com/docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with: with:
context: ./apps/log-exporter context: ./apps/log-exporter
file: ./apps/log-exporter/Dockerfile file: ./apps/log-exporter/Dockerfile
push: true push: true
tags: | tags: |
${{ env.REGISTRY }}/xpeditis-log-exporter:preprod
${{ env.REGISTRY }}/xpeditis-log-exporter:preprod-${{ steps.sha.outputs.short }} ${{ env.REGISTRY }}/xpeditis-log-exporter:preprod-${{ steps.sha.outputs.short }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-log-exporter:buildcache cache-from: type=registry,ref=${{ env.REGISTRY }}/xpeditis-log-exporter:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-log-exporter:buildcache,mode=max cache-to: type=registry,ref=${{ env.REGISTRY }}/xpeditis-log-exporter:buildcache,mode=max
platforms: linux/amd64,linux/arm64 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 ────────────────────────────────────────── # ── 5. Deploy via Portainer ──────────────────────────────────────────
deploy: deploy:
name: Deploy to Preprod name: Deploy to Preprod
runs-on: ubuntu-latest runs-on: xpeditis-deploy
needs: [build-backend, build-frontend, build-log-exporter] needs: [build-backend, build-frontend, build-log-exporter, image-security]
environment: preprod
steps: 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 - name: Deploy backend
run: | 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" echo "Portainer response: HTTP $HTTP_CODE"
if [[ "$HTTP_CODE" != "2"* ]]; then if [[ "$HTTP_CODE" != "2"* ]]; then
echo "ERROR: Portainer webhook failed with HTTP $HTTP_CODE" echo "ERROR: Portainer webhook failed with HTTP $HTTP_CODE"
@ -266,7 +350,7 @@ jobs:
run: sleep 20 run: sleep 20
- name: Deploy frontend - name: Deploy frontend
run: | 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" echo "Portainer response: HTTP $HTTP_CODE"
if [[ "$HTTP_CODE" != "2"* ]]; then if [[ "$HTTP_CODE" != "2"* ]]; then
echo "ERROR: Portainer webhook failed with HTTP $HTTP_CODE" echo "ERROR: Portainer webhook failed with HTTP $HTTP_CODE"
@ -274,6 +358,27 @@ jobs:
fi fi
echo "Frontend webhook triggered." 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 ──────────────────────────────────────────────────── # ── Notifications ────────────────────────────────────────────────────
notify-success: notify-success:
name: Notify Success name: Notify Success
@ -298,14 +403,14 @@ jobs:
notify-failure: notify-failure:
name: Notify Failure name: Notify Failure
runs-on: ubuntu-latest 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() if: failure()
steps: steps:
- run: | - run: |
curl -s -H "Content-Type: application/json" -d '{ curl -s -H "Content-Type: application/json" -d '{
"embeds": [{ "embeds": [{
"title": "❌ Preprod Pipeline Failed", "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, "color": 15158332,
"fields": [ "fields": [
{"name": "Author", "value": "${{ github.actor }}", "inline": true}, {"name": "Author", "value": "${{ github.actor }}", "inline": true},

View File

@ -6,14 +6,19 @@ on:
pull_request: pull_request:
branches: [dev] branches: [dev]
concurrency:
group: dev-ci-${{ github.ref }}
cancel-in-progress: true
env: env:
NODE_VERSION: '20' NODE_VERSION: '22'
jobs: 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: backend-quality:
name: Backend — Lint name: Backend — Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -21,14 +26,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm run lint -- --no-fix
- run: npm install --legacy-peer-deps
- run: npm run lint
frontend-quality: frontend-quality:
name: Frontend — Lint & Type-check name: Frontend — Lint & Type-check
@ -37,12 +42,12 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm run lint - run: npm run lint
- run: npm run type-check - run: npm run type-check
@ -55,14 +60,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm test -- --ci --runInBand
- run: npm install --legacy-peer-deps
- run: npm test -- --passWithNoTests
frontend-tests: frontend-tests:
name: Frontend — Unit Tests name: Frontend — Unit Tests
@ -72,19 +77,19 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm test -- --passWithNoTests - run: npm test -- --ci --runInBand
notify-failure: notify-failure:
name: Notify Failure name: Notify Failure
runs-on: ubuntu-latest 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() if: failure()
steps: steps:
- name: Discord - name: Discord

View File

@ -2,20 +2,25 @@ name: PR Checks
# Required status checks — configure these in branch protection rules. # Required status checks — configure these in branch protection rules.
# PRs to preprod : lint + type-check + unit tests + integration tests # 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: on:
pull_request: pull_request:
branches: [preprod, main] branches: [preprod, main]
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
env: env:
NODE_VERSION: '20' NODE_VERSION: '22'
jobs: 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: backend-quality:
name: Backend — Lint name: Backend — Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -23,14 +28,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm run lint -- --no-fix
- run: npm install --legacy-peer-deps
- run: npm run lint
frontend-quality: frontend-quality:
name: Frontend — Lint & Type-check name: Frontend — Lint & Type-check
@ -39,12 +44,12 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm run lint - run: npm run lint
- run: npm run type-check - run: npm run type-check
@ -57,14 +62,14 @@ jobs:
run: run:
working-directory: apps/backend working-directory: apps/backend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json - run: npm test -- --ci --runInBand
- run: npm install --legacy-peer-deps
- run: npm test -- --passWithNoTests
frontend-tests: frontend-tests:
name: Frontend — Unit Tests name: Frontend — Unit Tests
@ -74,22 +79,20 @@ jobs:
run: run:
working-directory: apps/frontend working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci --legacy-peer-deps - run: npm ci --legacy-peer-deps
- run: npm test -- --passWithNoTests - run: npm test -- --ci --runInBand
# Integration tests — PRs to preprod only # Integration tests validate the actual merge candidate for both branches.
# Code going to main was already integration-tested when it passed through preprod
integration-tests: integration-tests:
name: Backend — Integration Tests name: Backend — Integration Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: backend-tests needs: backend-tests
if: github.base_ref == 'preprod'
defaults: defaults:
run: run:
working-directory: apps/backend working-directory: apps/backend
@ -106,8 +109,6 @@ jobs:
--health-interval 5s --health-interval 5s
--health-timeout 5s --health-timeout 5s
--health-retries 10 --health-retries 10
ports:
- 5432:5432
redis: redis:
image: redis:7-alpine image: redis:7-alpine
options: >- options: >-
@ -115,31 +116,34 @@ jobs:
--health-interval 5s --health-interval 5s
--health-timeout 5s --health-timeout 5s
--health-retries 10 --health-retries 10
ports:
- 6379:6379
steps: steps:
- uses: actions/checkout@v4 - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@v4 with:
persist-credentials: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'npm' - run: npm ci --legacy-peer-deps
cache-dependency-path: apps/backend/package-lock.json
- run: npm install --legacy-peer-deps
- name: Run integration tests - name: Run integration tests
env: env:
NODE_ENV: test 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_PORT: 5432
DATABASE_USER: xpeditis_test DATABASE_USER: xpeditis_test
DATABASE_PASSWORD: xpeditis_test_password DATABASE_PASSWORD: xpeditis_test_password
DATABASE_NAME: xpeditis_test DATABASE_NAME: xpeditis_test
DATABASE_SYNCHRONIZE: 'false' DATABASE_SYNCHRONIZE: 'false'
REDIS_HOST: localhost REDIS_HOST: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
REDIS_PASSWORD: '' REDIS_PASSWORD: ''
JWT_SECRET: test-secret-key-ci JWT_SECRET: test-secret-key-ci
SMTP_HOST: localhost SMTP_HOST: localhost
SMTP_PORT: 1025 SMTP_PORT: 1025
SMTP_FROM: test@xpeditis.com SMTP_FROM: test@xpeditis.com
run: npm run test:integration -- --passWithNoTests run: npm run test:integration -- --ci --runInBand

View File

@ -1,7 +1,7 @@
# =============================================== # ===============================================
# Stage 1: Dependencies Installation # Stage 1: Dependencies Installation
# =============================================== # ===============================================
FROM node:20-alpine AS dependencies FROM node:22-alpine AS dependencies
# Install build dependencies # Install build dependencies
RUN apk add --no-cache python3 make g++ libc6-compat RUN apk add --no-cache python3 make g++ libc6-compat
@ -19,7 +19,7 @@ RUN npm ci --legacy-peer-deps
# =============================================== # ===============================================
# Stage 2: Build Application # Stage 2: Build Application
# =============================================== # ===============================================
FROM node:20-alpine AS builder FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
@ -38,7 +38,7 @@ RUN npm prune --production --legacy-peer-deps
# =============================================== # ===============================================
# Stage 3: Production Image # Stage 3: Production Image
# =============================================== # ===============================================
FROM node:20-alpine AS production FROM node:22-alpine AS production
# Install dumb-init for proper signal handling # Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init RUN apk add --no-cache dumb-init

View File

@ -1,7 +1,7 @@
# =============================================== # ===============================================
# Stage 1: Dependencies Installation # Stage 1: Dependencies Installation
# =============================================== # ===============================================
FROM node:20-alpine AS dependencies FROM node:22-alpine AS dependencies
# Install build dependencies # Install build dependencies
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
@ -18,7 +18,7 @@ RUN npm ci --legacy-peer-deps
# =============================================== # ===============================================
# Stage 2: Build Application # Stage 2: Build Application
# =============================================== # ===============================================
FROM node:20-alpine AS builder FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
@ -48,7 +48,7 @@ RUN npm run build
# =============================================== # ===============================================
# Stage 3: Production Image # Stage 3: Production Image
# =============================================== # ===============================================
FROM node:20-alpine AS production FROM node:22-alpine AS production
# Install dumb-init for proper signal handling # Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init curl RUN apk add --no-cache dumb-init curl

View File

@ -1,9 +1,9 @@
FROM node:20-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app
COPY package.json ./ COPY package.json package-lock.json ./
RUN npm install --omit=dev RUN npm ci --omit=dev
COPY src/ ./src/ COPY src/ ./src/

965
apps/log-exporter/package-lock.json generated Normal file
View File

@ -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"
}
}
}
}

161
docs/CI-CD-SECURITY.md Normal file
View File

@ -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-<SHA complet>` 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/).

8
renovate.json Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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()

View File

@ -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