From e50fc4824e6a6882d5d9615f51d84d536e2a642c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 28 Sep 2026 17:24:57 +0200 Subject: [PATCH 01/14] docs: define environment and secrets management Document mise/fnox and restrict the initializer to the frozen Fastify legacy. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- AGENTS.md | 10 ++++---- ENVIRONMENTS.md | 8 +++--- README.md | 13 ++++------ ci/scripts/init-env.sh | 17 +++++++------ .../0001-mise-fnox-environment-management.md | 24 ++++++++++++++++++ docs/environment-variables.md | 25 +++++++++++++++++++ 6 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 docs/adr/0001-mise-fnox-environment-management.md create mode 100644 docs/environment-variables.md diff --git a/AGENTS.md b/AGENTS.md index 51812e86a6..307ccbe469 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,11 +53,11 @@ always filter by concrete id. `ProjectRole`/`Repository` foreign keys do not cas ## Environment config -- Files: `.env`, `.env.docker`, `.env.integ` in `apps/client/`, `apps/server/`, `apps/server-nestjs/` -- Templates: `*-example` suffix (not `.example`; git-tracked), active files gitignored -- Override chain (weakest to strongest): `.env` < `.env.docker` (if DOCKER=true) < `.env.integ` (if INTEGRATION=true) < explicit env vars -- Server loading: `apps/server/src/utils/env.ts` | Client: `apps/client/vite.config.ts` -- `ci/scripts/init-env.sh` copies `*-example` to active equivalents (non-destructive) +- `mise` owns tool versions, supported tasks and public configuration; `fnox` resolves secrets for child processes only. +- `apps/client/` and `apps/server-nestjs/` must not use `.env*` files or dotenv loaders. +- `apps/server/` is a frozen exception: its `.env*`, loaders and Compose `env_file` remain until its removal. +- `ci/scripts/init-env.sh` initializes that legacy exception only and must be deleted with it. +- The variable classification and profiles are in `docs/environment-variables.md`. ## Testing diff --git a/ENVIRONMENTS.md b/ENVIRONMENTS.md index 6c8cd9c397..35b50ed30c 100644 --- a/ENVIRONMENTS.md +++ b/ENVIRONMENTS.md @@ -53,11 +53,13 @@ Le mécanisme de surcharge des différentes configurations fonctionne de cette m var d'env settée explicitement -> fichier .env.docker (si contexte docker) -> fichier .env.integ (si INTEGRATION=true) -> fichier .env ``` -## Prégénération des fichiers .env, .env.docker, et .env.integ +## Transition du legacy Fastify -Un script permet de copier facilement les fichiers `.env*-example` en leur équivalent `.env*`: [`./ci/scripts/init-env.sh`](./ci/scripts/init-env.sh). +`ci/scripts/init-env.sh` ne sert qu'à `apps/server`, le backend Fastify gelé. Il conserve temporairement ses `.env*` jusqu'à la suppression de ce service. -> Il faut ensuite remplir ces fichiers, car ils ne sont là que simplement copiés avec les valeurs par défaut +Le client et `server-nestjs` migrent vers `mise` pour la configuration publique et `fnox` pour les secrets : leurs commandes ne génèrent pas de fichier `.env`. + +La décision et le registre de variables sont documentés dans [`docs/adr/0001-mise-fnox-environment-management.md`](docs/adr/0001-mise-fnox-environment-management.md) et [`docs/environment-variables.md`](docs/environment-variables.md). ## Configuration pour le développement entièrement en local diff --git a/README.md b/README.md index e056034066..734d573160 100644 --- a/README.md +++ b/README.md @@ -111,17 +111,14 @@ Le développement s'effectue à l'aide de Docker *(le client et le serveur peuve Une fois le projet cloné, lancez les commandes suivantes dans votre terminal pour installer le projet et construire l'ensemble de l'application `console` : ```shell -# Installer les dépendances du projet -pnpm install +# Installer les outils déclarés par le projet (Node, pnpm et fnox) +mise install -# Créer les fichiers d'environnement exemples -./ci/scripts/init-env.sh - -# Générer le client Prisma côté serveur -pnpm --filter @cpn-console/server run db:generate +# Initialiser les secrets locaux de démonstration et le bridge legacy apps/server +mise run setup # Construire tous les paquets applicatifs -pnpm build +mise exec -- pnpm build ``` #### Lancer l'application diff --git a/ci/scripts/init-env.sh b/ci/scripts/init-env.sh index 0c6c5d3feb..0b69a9288a 100755 --- a/ci/scripts/init-env.sh +++ b/ci/scripts/init-env.sh @@ -12,7 +12,8 @@ i=1 PROJECT_DIR="$(git rev-parse --show-toplevel)" # Declare script helper -TEXT_HELPER="\nThis script aims to copy .env-example files into .env files at project initialization. +TEXT_HELPER="\nTemporary bridge for the frozen apps/server legacy: copies its .env-example files into .env files. +Remove this script when apps/server is removed from the repository. Following flags are available: -h Print script help\n\n" @@ -32,12 +33,14 @@ do done -find $PROJECT_DIR -type f -name ".env*-example" -or -name "*-example.yaml" | while read f; do - if [ ! -f "${f/-example/}" ]; then - printf "\n${red}Copy${no_color}: '$f' - ${red}to${no_color}: '${f/-example/}'\n" - cp "$f" "${f/-example/}" +# Temporary bridge for the frozen Fastify legacy only. +# Remove this script when apps/server is removed from the repository. +find "$PROJECT_DIR/apps/server" -type f -name ".env*-example" -print0 | while IFS= read -r -d '' file; do + target="${file/-example/}" + if [ ! -f "$target" ]; then + printf "\n${red}Copy${no_color}: '%s'\n${red}to${no_color}: '%s'\n" "$file" "$target" + cp "$file" "$target" else - printf "\nFile '${f/-example/}' already exists\n" + printf "\nFile '%s' already exists\n" "$target" fi done diff --git a/docs/adr/0001-mise-fnox-environment-management.md b/docs/adr/0001-mise-fnox-environment-management.md new file mode 100644 index 0000000000..13ed95ce1f --- /dev/null +++ b/docs/adr/0001-mise-fnox-environment-management.md @@ -0,0 +1,24 @@ +# ADR 0001 — mise et fnox pour la configuration et les secrets + +## Statut + +Proposé pour validation en séance plénière. + +## Contexte + +La Console mélange configuration et secrets dans des fichiers `.env*` lus par les applications, Docker Compose, Prisma et la CI. Les sources et les précédences diffèrent selon le mode d’exécution, rendant la configuration difficile à auditer et à reproduire. + +## Décision + +`mise` est l’unique point d’entrée supporté et fournit les outils, tâches et variables publiques. `fnox` injecte les secrets uniquement dans le processus enfant de `fnox exec`. Le fournisseur local par défaut est `pass`, sous `fnox/console/`. Les profils CI lisent uniquement les secrets nécessaires au processus qui les reçoit depuis GitHub Actions et n’installent pas `pass`. + +Les fichiers `.env*` sont interdits hors de `apps/server`. Le legacy Fastify est gelé : ses fichiers, chargeurs et `env_file` Compose restent inchangés jusqu’à sa suppression du dépôt. + +## Conséquences + +Les appels directs à `pnpm` et `docker compose` ne sont plus des points d’entrée documentés. Le client ne reçoit qu’une liste blanche de variables publiques. Un changement de fournisseur fnox reste localisé aux profils fnox et ne change pas les tâches applicatives. + +## Alternatives écartées + +- Conserver les `.env*` : ne résout ni la multiplicité des sources, ni la confusion entre secrets et configuration. +- Nix et SecretSpec : alternative retenue seulement si `mise` et `fnox` ne répondent plus au besoin ; elle est hors périmètre de cette migration. diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000000..1263f9ef2b --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,25 @@ +# Registre des variables d’environnement + +Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. + +| Classe | Variables | Source | Consommateurs | +| --- | --- | --- | --- | +| Configuration publique commune | `NODE_ENV`, `CI`, `APP_VERSION`, `SERVER_HOST`, `SERVER_PORT`, `NESTJS_SERVER_HOST`, `NESTJS_SERVER_PORT`, `CONTACT_EMAIL`, `PROJECTS_ROOT_DIR`, `LOG_LEVEL` | `mise` | client, server-nestjs, Docker Compose, CI | +| Configuration publique navigateur | `SERVER_HOST`, `SERVER_PORT`, `CLIENT_PORT`, `KEYCLOAK_PROTOCOL`, `KEYCLOAK_DOMAIN`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_REDIRECT_URI`, `OPENCDS_ENABLED`, `CONTACT_EMAIL` | `mise` | client | +| Activation et endpoints de services | `USE_ARGOCD`, `USE_GITLAB`, `USE_NEXUS`, `USE_HARBOR`, `USE_SONARQUBE`, `USE_SERVICE_CHAIN`, `USE_VAULT`, `USE_OBSERVABILITY`, `ARGO_NAMESPACE`, `ARGOCD_URL`, `ARGOCD_INTERNAL_URL`, `ARGOCD_EXTRA_REPOSITORIES`, `ARGOCD_SHARED_SOURCE_REPOSITORIES`, `GITLAB_URL`, `GITLAB_INTERNAL_URL`, `GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS`, `GITLAB_MIRROR_TOKEN_ROTATION_THRESHOLD_DAYS`, `GITLAB__SECRET_EXPOSE_INTERNAL_URL`, `HARBOR_URL`, `HARBOR_INTERNAL_URL`, `HARBOR_PROJECT_SLUG_CACHE_TTL_MS`, `HARBOR_RETENTION_CRON`, `HARBOR_ROBOT_EXPIRATION_DAYS`, `HARBOR_ROBOT_ROTATION_THRESHOLD_DAYS`, `HARBOR_RULE_COUNT`, `HARBOR_RULE_TEMPLATE`, `NEXUS_URL`, `NEXUS_INTERNAL_URL`, `NEXUS__SECRET_EXPOSE_INTERNAL_URL`, `SONARQUBE_URL`, `SONARQUBE_INTERNAL_URL`, `GRAFANA_URL`, `DSO_OBSERVABILITY_CHART_VERSION`, `VAULT_URL`, `VAULT_INTERNAL_URL`, `VAULT_KV_NAME`, `VAULT__DEPLOY_VAULT_CONNECTION_IN_NS`, `OPENCDS_URL`, `OPENCDS_INTERNAL_URL`, `OPENCDS_API_TLS_REJECT_UNAUTHORIZED`, `DSO_ENV_CHART_VERSION`, `DSO_NS_CHART_VERSION`, `KUBECONFIG_HOST_PATH`, `KUBECONFIG_PATH`, `KUBECONFIG_CTX`, `EXTERNAL_PLUGINS_DIR_HOST_PATH` | `mise` | server-nestjs, Docker Compose | +| Secrets locaux | `SESSION_SECRET`, `DB_URL`, `KEYCLOAK_CLIENT_SECRET`, `KEYCLOAK_ADMIN`, `KEYCLOAK_ADMIN_PASSWORD`, `OPENCDS_API_TOKEN` | `fnox` / `pass` | server-nestjs, Docker Compose | +| Secrets d’intégration | `GITLAB_TOKEN`, `HARBOR_ADMIN`, `HARBOR_ADMIN_PASSWORD`, `NEXUS_ADMIN`, `NEXUS_ADMIN_PASSWORD`, `SONAR_API_TOKEN`, `VAULT_TOKEN` | `fnox` / fournisseur du profil | server-nestjs, Docker Compose | +| Secrets CI | secrets GitHub Actions déjà déclarés par le workflow (`SONAR_*`, `ARGOCD_TOKEN`, clés GitHub App et Helm) | GitHub Actions | étape consommatrice uniquement | + +## Profils + +| Profil | Usage | Source des secrets | +| --- | --- | --- | +| défaut | développement local natif | `pass` sous `fnox/console/local/` | +| `docker` | stack Docker locale | `pass` sous `fnox/console/local/` | +| `integ` | environnement d’intégration | `pass` ou futur fournisseur fnox du profil | +| `ci` | tests déterministes | valeurs de fixture fnox ; secrets GitHub Actions restent limités à leur étape | + +## Exception legacy + +`apps/server` reste hors de ce registre pendant sa période de décommissionnement. Ses `.env*`, templates et chargeurs sont conservés inchangés et `ci/scripts/init-env.sh` ne sert plus qu’à eux. From 1ee8c6c1795af1efb487baf85a787eec4e24117c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 28 Sep 2026 17:44:45 +0200 Subject: [PATCH 02/14] feat: add mise and fnox environment profiles Scope public configuration by consumer and bootstrap local pass secrets interactively. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- .gitignore | 6 +++ fnox.ci.toml | 7 ++++ fnox.integ.toml | 8 ++++ fnox.toml | 16 ++++++++ mise.ci.toml | 11 +++++ mise.docker.toml | 8 ++++ mise.integ.toml | 5 +++ mise.toml | 67 +++++++++++++++++++++++++++++++ scripts/bootstrap-secrets.sh | 32 +++++++++++++++ scripts/bootstrap-secrets.spec.sh | 30 ++++++++++++++ 10 files changed, 190 insertions(+) create mode 100644 fnox.ci.toml create mode 100644 fnox.integ.toml create mode 100644 fnox.toml create mode 100644 mise.ci.toml create mode 100644 mise.docker.toml create mode 100644 mise.integ.toml create mode 100644 mise.toml create mode 100755 scripts/bootstrap-secrets.sh create mode 100755 scripts/bootstrap-secrets.spec.sh diff --git a/.gitignore b/.gitignore index 090b202522..4c0dc0ff06 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,12 @@ node_modules .env.codespace .env.docker +# local mise/fnox overrides +mise.local.toml +mise.*.local.toml +fnox.local.toml +.fnox.local.toml + # init-db apps/server/src/init/db/imports/data diff --git a/fnox.ci.toml b/fnox.ci.toml new file mode 100644 index 0000000000..e11673dd6e --- /dev/null +++ b/fnox.ci.toml @@ -0,0 +1,7 @@ +[secrets] +SESSION_SECRET = { default = "a-very-strong-secret-with-more-than-32-char" } +DB_URL = { default = "postgresql://admin:admin@postgres:5432/dso-console-db?schema=public" } +KEYCLOAK_CLIENT_SECRET = { default = "client-secret-backend" } +KEYCLOAK_ADMIN = { default = "admin" } +KEYCLOAK_ADMIN_PASSWORD = { default = "admin" } +OPENCDS_API_TOKEN = { default = "token" } diff --git a/fnox.integ.toml b/fnox.integ.toml new file mode 100644 index 0000000000..0c5b595d07 --- /dev/null +++ b/fnox.integ.toml @@ -0,0 +1,8 @@ +[secrets] +GITLAB_TOKEN = { provider = "pass", value = "integ/GITLAB_TOKEN" } +HARBOR_ADMIN = { provider = "pass", value = "integ/HARBOR_ADMIN" } +HARBOR_ADMIN_PASSWORD = { provider = "pass", value = "integ/HARBOR_ADMIN_PASSWORD" } +NEXUS_ADMIN = { provider = "pass", value = "integ/NEXUS_ADMIN" } +NEXUS_ADMIN_PASSWORD = { provider = "pass", value = "integ/NEXUS_ADMIN_PASSWORD" } +SONAR_API_TOKEN = { provider = "pass", value = "integ/SONAR_API_TOKEN" } +VAULT_TOKEN = { provider = "pass", value = "integ/VAULT_TOKEN" } diff --git a/fnox.toml b/fnox.toml new file mode 100644 index 0000000000..ad532a28fb --- /dev/null +++ b/fnox.toml @@ -0,0 +1,16 @@ +#:schema https://fnox.jdx.dev/schema.json +root = true +env = "exec" +if_missing = "error" + +[providers.pass] +type = "password-store" +prefix = "fnox/console/" + +[secrets] +SESSION_SECRET = { provider = "pass", value = "local/SESSION_SECRET" } +DB_URL = { provider = "pass", value = "local/DB_URL" } +KEYCLOAK_CLIENT_SECRET = { provider = "pass", value = "local/KEYCLOAK_CLIENT_SECRET" } +KEYCLOAK_ADMIN = { provider = "pass", value = "local/KEYCLOAK_ADMIN" } +KEYCLOAK_ADMIN_PASSWORD = { provider = "pass", value = "local/KEYCLOAK_ADMIN_PASSWORD" } +OPENCDS_API_TOKEN = { provider = "pass", value = "local/OPENCDS_API_TOKEN" } diff --git a/mise.ci.toml b/mise.ci.toml new file mode 100644 index 0000000000..e11a3909b5 --- /dev/null +++ b/mise.ci.toml @@ -0,0 +1,11 @@ +[env] +CI = "true" +FNOX_PROFILE = "ci" +NODE_ENV = "production" +DEV_SETUP = "true" +CLIENT_SERVER_HOST = "nginx-strangler" +CLIENT_SERVER_PORT = "8080" +CLIENT_OPENCDS_ENABLED = "true" +NESTJS_SERVER_HOST = "0.0.0.0" +NESTJS_SERVER_PORT = "3001" +NESTJS_KEYCLOAK_DOMAIN = "keycloak:8080" diff --git a/mise.docker.toml b/mise.docker.toml new file mode 100644 index 0000000000..20ca697525 --- /dev/null +++ b/mise.docker.toml @@ -0,0 +1,8 @@ +[env] +DOCKER = "true" +CLIENT_SERVER_HOST = "nginx-strangler" +CLIENT_SERVER_PORT = "8080" +CLIENT_OPENCDS_ENABLED = "true" +NESTJS_SERVER_HOST = "0.0.0.0" +NESTJS_SERVER_PORT = "3001" +NESTJS_KEYCLOAK_DOMAIN = "keycloak:8080" diff --git a/mise.integ.toml b/mise.integ.toml new file mode 100644 index 0000000000..6fc7add2cc --- /dev/null +++ b/mise.integ.toml @@ -0,0 +1,5 @@ +[env] +INTEGRATION = "true" +DEV_SETUP = "false" +NESTJS_SERVER_HOST = "0.0.0.0" +NESTJS_SERVER_PORT = "3001" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..60fdae870f --- /dev/null +++ b/mise.toml @@ -0,0 +1,67 @@ +min_version = "2026.5.15" + +[tools] +node = "26.7.0" +"npm:pnpm" = "11.8.0" +fnox = "1.36.0" + +[env] +NODE_ENV = "development" +DEV_SETUP = "true" +CLIENT_SERVER_HOST = "localhost" +CLIENT_SERVER_PORT = "4000" +CLIENT_PORT = "8080" +CLIENT_KEYCLOAK_PROTOCOL = "http" +CLIENT_KEYCLOAK_DOMAIN = "localhost:8090" +CLIENT_KEYCLOAK_REALM = "dso" +CLIENT_KEYCLOAK_CLIENT_ID = "dso-console-frontend" +CLIENT_KEYCLOAK_REDIRECT_URI = "http://localhost:8080" +CLIENT_OPENCDS_ENABLED = "false" +CLIENT_CONTACT_EMAIL = "cloudpinative-relations@interieur.gouv.fr" +NESTJS_SERVER_HOST = "localhost" +NESTJS_SERVER_PORT = "3001" +NESTJS_KEYCLOAK_PROTOCOL = "http" +NESTJS_KEYCLOAK_DOMAIN = "localhost:8090" +NESTJS_KEYCLOAK_REALM = "dso" +NESTJS_KEYCLOAK_CLIENT_ID = "dso-console-backend" +NESTJS_KEYCLOAK_REDIRECT_URI = "http://localhost:8080" +NESTJS_CONTACT_EMAIL = "cloudpinative-relations@interieur.gouv.fr" + +[tasks.setup] +run = "./ci/scripts/init-env.sh && ./scripts/bootstrap-secrets.sh" + +[tasks.dev] +run = "docker compose -f docker/docker-compose.local.yml up -d --remove-orphans" + +[tasks."server-nestjs:dev"] +run = "SERVER_HOST=$NESTJS_SERVER_HOST SERVER_PORT=$NESTJS_SERVER_PORT KEYCLOAK_PROTOCOL=$NESTJS_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$NESTJS_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$NESTJS_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$NESTJS_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$NESTJS_KEYCLOAK_REDIRECT_URI CONTACT_EMAIL=$NESTJS_CONTACT_EMAIL fnox exec -- pnpm --filter server-nestjs run start:dev" + +[tasks."client:dev"] +run = "SERVER_HOST=$CLIENT_SERVER_HOST SERVER_PORT=$CLIENT_SERVER_PORT CLIENT_PORT=$CLIENT_PORT KEYCLOAK_PROTOCOL=$CLIENT_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$CLIENT_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$CLIENT_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$CLIENT_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$CLIENT_KEYCLOAK_REDIRECT_URI OPENCDS_ENABLED=$CLIENT_OPENCDS_ENABLED CONTACT_EMAIL=$CLIENT_CONTACT_EMAIL pnpm --filter client run dev" + +[tasks.lint] +run = "pnpm lint" + +[tasks."test:unit"] +run = "fnox exec -- pnpm test" + +[tasks."docker:dev"] +run = "mise -E docker run docker:dev-inner" + +[tasks."docker:dev-inner"] +hide = true +run = "fnox exec -- docker compose -f docker/docker-compose.dev.yml up -d --remove-orphans" + +[tasks.integ] +run = "mise -E integ run integ-inner" + +[tasks."integ-inner"] +hide = true +run = "fnox -P integ exec -- docker compose -f docker/docker-compose.local.yml up -d --remove-orphans postgres pgadmin" + +[tasks."docker:integ"] +run = "mise -E integ run docker:integ-inner" + +[tasks."docker:integ-inner"] +hide = true +run = "fnox -P integ exec -- docker compose -f docker/docker-compose.integ.yml up -d --remove-orphans" diff --git a/scripts/bootstrap-secrets.sh b/scripts/bootstrap-secrets.sh new file mode 100755 index 0000000000..29cf8133ab --- /dev/null +++ b/scripts/bootstrap-secrets.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! command -v pass >/dev/null 2>&1; then + printf 'pass is required; install and initialize it with your GPG identity before running this command.\n' >&2 + exit 1 +fi + +if ! command -v fnox >/dev/null 2>&1; then + printf 'fnox is required; run mise install before running this command.\n' >&2 + exit 1 +fi + +printf 'Initialize missing local demonstration secrets in pass? [y/N] ' +read -r confirmation +if [ "$confirmation" != "y" ] && [ "$confirmation" != "Y" ]; then + printf 'Secret initialization cancelled.\n' + exit 0 +fi + +while IFS='|' read -r name value; do + [ -n "$name" ] || continue + fnox get "$name" >/dev/null 2>&1 || fnox set "$name" "$value" --provider pass +done <<'SECRETS' +SESSION_SECRET|a-very-strong-secret-with-more-than-32-char +DB_URL|postgresql://admin:admin@localhost:5432/dso-console-db?schema=public +KEYCLOAK_CLIENT_SECRET|client-secret-backend +KEYCLOAK_ADMIN|admin +KEYCLOAK_ADMIN_PASSWORD|admin +OPENCDS_API_TOKEN|token +SECRETS diff --git a/scripts/bootstrap-secrets.spec.sh b/scripts/bootstrap-secrets.spec.sh new file mode 100755 index 0000000000..b88811eb6f --- /dev/null +++ b/scripts/bootstrap-secrets.spec.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/pass" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + +cat > "$tmp_dir/bin/fnox" <<'EOF' +#!/usr/bin/env bash +if [ "$1" = "get" ]; then + exit 1 +fi +printf '%s\n' "$*" >> "$FNOX_CALLS" +EOF + +chmod +x "$tmp_dir/bin/pass" "$tmp_dir/bin/fnox" +export FNOX_CALLS="$tmp_dir/fnox.calls" +export PATH="$tmp_dir/bin:$PATH" +printf 'y\n' | "$project_dir/scripts/bootstrap-secrets.sh" + +test "$(grep -c '^set ' "$tmp_dir/fnox.calls")" -eq 6 +grep -Fqx 'set SESSION_SECRET a-very-strong-secret-with-more-than-32-char --provider pass' "$tmp_dir/fnox.calls" +grep -Fqx 'set OPENCDS_API_TOKEN token --provider pass' "$tmp_dir/fnox.calls" From c6d5ec9e478925318b7e7023d0996b500017cbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 28 Sep 2026 17:46:54 +0200 Subject: [PATCH 03/14] refactor(server-nestjs): remove dotenv loading Inject NestJS test configuration through the CI mise and fnox profile. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- apps/server-nestjs/.env-example | 55 -------- apps/server-nestjs/.env.docker-example | 67 --------- apps/server-nestjs/.env.integ-example | 132 ------------------ apps/server-nestjs/prisma.config.ts | 14 -- apps/server-nestjs/src/main.module.ts | 2 - .../src/utils/dotenv.utils.spec.ts | 35 ----- apps/server-nestjs/src/utils/dotenv.utils.ts | 26 ---- apps/server-nestjs/test/argocd.e2e-spec.ts | 3 +- apps/server-nestjs/test/gitlab.e2e-spec.ts | 3 +- apps/server-nestjs/test/keycloak.e2e-spec.ts | 3 +- apps/server-nestjs/test/log.e2e-spec.ts | 3 +- apps/server-nestjs/test/nexus.e2e-spec.ts | 3 +- .../test/observability.e2e-spec.ts | 3 +- .../test/project-bulk.e2e-spec.ts | 3 +- .../test/project-hooks.e2e-spec.ts | 3 +- .../test/project-members.e2e-spec.ts | 3 +- .../test/project-roles.e2e-spec.ts | 3 +- .../test/project-secrets.e2e-spec.ts | 3 +- .../test/project-services.e2e-spec.ts | 3 +- apps/server-nestjs/test/project.e2e-spec.ts | 3 +- apps/server-nestjs/test/registry.e2e-spec.ts | 3 +- apps/server-nestjs/test/sonarqube.e2e-spec.ts | 3 +- apps/server-nestjs/test/vault.e2e-spec.ts | 3 +- apps/server-nestjs/test/zone.e2e-spec.ts | 3 +- mise.toml | 7 + 25 files changed, 24 insertions(+), 365 deletions(-) delete mode 100644 apps/server-nestjs/.env-example delete mode 100644 apps/server-nestjs/.env.docker-example delete mode 100644 apps/server-nestjs/.env.integ-example delete mode 100644 apps/server-nestjs/src/utils/dotenv.utils.spec.ts delete mode 100644 apps/server-nestjs/src/utils/dotenv.utils.ts diff --git a/apps/server-nestjs/.env-example b/apps/server-nestjs/.env-example deleted file mode 100644 index c1a05eaea5..0000000000 --- a/apps/server-nestjs/.env-example +++ /dev/null @@ -1,55 +0,0 @@ -# Active le mode développement (initialise les données de démo au démarrage) -DEV_SETUP="true" -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# HOME=/home/node -# Secret utilisé pour signer les cookies de session (min. 32 caractères) -SESSION_SECRET=a-very-strong-secret-with-more-than-32-char - -# --- Configuration Keycloak --- -# Domaine (host:port) du serveur Keycloak pour l'authentification -KEYCLOAK_DOMAIN=localhost:8090 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http | https) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté backend -KEYCLOAK_CLIENT_ID=dso-console-backend -# Secret du client Keycloak backend (confidentiel) -KEYCLOAK_CLIENT_SECRET=client-secret-backend -# Identifiant de l'administrateur Keycloak sur le royaume KEYCLOAK_REALM -KEYCLOAK_ADMIN=admin -# Mot de passe de l'administrateur Keycloak sur le royaume KEYCLOAK_REALM -KEYCLOAK_ADMIN_PASSWORD=admin -# Client utilisé par le client admin pour s'authentifier sur le royaume master (requis) -KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# JWKS cache TTL in milliseconds (default 300000 = 5 min) -KEYCLOAK_JWKS_CACHE_TTL_MS=300000 -# JWKS fetch timeout in milliseconds (default 1000 = 1 s) -KEYCLOAK_JWKS_TIMEOUT_MS=1000 -# Interface d'écoute du serveur backend -SERVER_HOST=localhost -# Port d'écoute du serveur backend -SERVER_PORT=3001 -# URL de connexion à la base de données PostgreSQL (format Prisma) -DB_URL=postgresql://admin:admin@localhost:5432/dso-console-db?schema=public -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr - -# --- Activation des plugins externes --- -# Mettez "true" et renseignez les variables ci-dessus pour activer chaque plugin. -USE_ARGOCD=false -USE_GITLAB=false -USE_NEXUS=false -USE_HARBOR=false -USE_SONARQUBE=false -USE_VAULT=false -USE_OBSERVABILITY=false - -# --- Configuration Observability (Grafana) --- -# URL publique de Grafana -GRAFANA_URL=https://grafana.example.com -# Version du chart Helm dso-observability -DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7 diff --git a/apps/server-nestjs/.env.docker-example b/apps/server-nestjs/.env.docker-example deleted file mode 100644 index 22aa728b7a..0000000000 --- a/apps/server-nestjs/.env.docker-example +++ /dev/null @@ -1,67 +0,0 @@ -# Indique que l'application tourne dans un conteneur Docker -DOCKER=true -# Active le mode développement (initialise les données de démo au démarrage) -DEV_SETUP="true" -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# Secret utilisé pour signer les cookies de session (min. 32 caractères) -SESSION_SECRET=a-very-strong-secret-with-more-than-32-char - -# --- Configuration Keycloak --- -# Domaine du Keycloak (nom du service Docker Compose + port interne) -KEYCLOAK_DOMAIN=keycloak:8080 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http en Docker local) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté backend -KEYCLOAK_CLIENT_ID=dso-console-backend -# Secret du client Keycloak backend (confidentiel) -KEYCLOAK_CLIENT_SECRET=client-secret-backend -# Identifiant de l'administrateur Keycloak -KEYCLOAK_ADMIN=admin -# Mot de passe de l'administrateur Keycloak -KEYCLOAK_ADMIN_PASSWORD=admin -# Client utilisé par le client admin pour s'authentifier sur le royaume master (requis) -KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# JWKS cache TTL in milliseconds (default 300000 = 5 min) -KEYCLOAK_JWKS_CACHE_TTL_MS=300000 -# JWKS fetch timeout in milliseconds (default 1000 = 1 s) -KEYCLOAK_JWKS_TIMEOUT_MS=1000 -# Interface d'écoute du serveur backend -SERVER_HOST=0.0.0.0 -# Port d'écoute du serveur dans le réseau Docker -SERVER_PORT=3001 -# URL de connexion PostgreSQL (utilise le nom de service Docker 'postgres') -DB_URL=postgresql://admin:admin@postgres:5432/dso-console-db?schema=public -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr - -# --- Configuration OpenCDS --- -# Token d'authentification pour l'API OpenCDS -OPENCDS_API_TOKEN=token -# Vérification du certificat TLS de l'API OpenCDS (true | false) -OPENCDS_API_TLS_REJECT_UNAUTHORIZED=false -# URL accessible à l'extérieur du cluster -OPENCDS_URL=http://dso-console_opencds-mockoon:3100/api/v1/health -# URL accessible seulement à l'interieur du cluster -OPENCDS_INTERNAL_URL=http://dso-console_opencds-mockoon:3100/api/v1/health - -# --- Activation des plugins externes --- -# Mettez "true" et renseignez les variables ci-dessus pour activer chaque plugin. -USE_ARGOCD=false -USE_GITLAB=false -USE_NEXUS=false -USE_HARBOR=false -USE_SONARQUBE=false -USE_SERVICE_CHAIN=true -USE_VAULT=false -USE_OBSERVABILITY=false - -# --- Configuration Observability (Grafana) --- -# URL publique de Grafana -GRAFANA_URL=https://grafana.example.com -# Version du chart Helm dso-observability -DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7 diff --git a/apps/server-nestjs/.env.integ-example b/apps/server-nestjs/.env.integ-example deleted file mode 100644 index a56c2ff131..0000000000 --- a/apps/server-nestjs/.env.integ-example +++ /dev/null @@ -1,132 +0,0 @@ -# Surcharges pour le branchement à un environnement d'intégration distant. -# Ces valeurs écrasent celles de .env et .env.docker quand INTEGRATION=true. - -# Désactive l'initialisation des données de démo (l'intégration a ses propres données) -DEV_SETUP="false" -# Active le mode intégration (charge ce fichier en surcharge) -INTEGRATION=true - -# --- Keycloak --- -# Protocole (généralement https en intégration) -KEYCLOAK_PROTOCOL=https -# Identifiant du client Keycloak backend sur l'environnement d'intégration -KEYCLOAK_CLIENT_ID= -# Secret du client Keycloak backend sur l'environnement d'intégration -KEYCLOAK_CLIENT_SECRET= -# Domaine du Keycloak d'intégration (ex: keycloak.dso.example.fr) -KEYCLOAK_DOMAIN= -# Royaume Keycloak d'intégration -KEYCLOAK_REALM= - -# JWKS cache TTL in milliseconds (default 300000 = 5 min) -KEYCLOAK_JWKS_CACHE_TTL_MS=300000 -# JWKS fetch timeout in milliseconds (default 1000 = 1 s) -KEYCLOAK_JWKS_TIMEOUT_MS=1000 - -# --- ArgoCD --- -# Namespace Kubernetes dans lequel ArgoCD est déployé -ARGO_NAMESPACE= -# URL de l'API ArgoCD -ARGOCD_URL= -# URL interne de l'API ArgoCD -ARGOCD_INTERNAL_URL= -# Sources autorisées pour tous les projets, séparées par des virgules. -# est remplacé par le slug du projet. -# Ex. pour le registre Harbor : oci://registry.example.com//** -ARGOCD_SHARED_SOURCE_REPOSITORIES= - -# --- GitLab --- -# Token d'accès personnel GitLab (scope api) -GITLAB_TOKEN= -# URL de l'instance GitLab -GITLAB_URL= -# URL interne de l'API Gitlab -GITLAB_INTERNAL_URL= -# Durée de validité (en jours) des tokens d'accès groupe utilisés pour le miroir -GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS=365 -# Seuil (en jours) de rotation préventive des tokens, strictement inférieur à GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS -GITLAB_MIRROR_TOKEN_ROTATION_THRESHOLD_DAYS=250 - -# --- Harbor --- -# Nom d'utilisateur admin Harbor -HARBOR_ADMIN= -# Mot de passe admin Harbor -HARBOR_ADMIN_PASSWORD= -# URL de l'instance Harbor -HARBOR_URL= -# URL interne de l'API Harbor -HARBOR_INTERNAL_URL= -# Durée de validité (en jours) des robots Harbor -HARBOR_ROBOT_EXPIRATION_DAYS=90 -# Seuil (en jours) de rotation préventive des secrets, strictement inférieur à HARBOR_ROBOT_EXPIRATION_DAYS -HARBOR_ROBOT_ROTATION_THRESHOLD_DAYS=60 - -# --- Keycloak (admin) --- -# Utilisateur admin Keycloak pour la gestion des royaumes/clients -KEYCLOAK_ADMIN= -# Mot de passe admin Keycloak -KEYCLOAK_ADMIN_PASSWORD= -# Client utilisé par le client admin pour s'authentifier sur le royaume master (requis) -KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -# URL complète de Keycloak (peut différer de KEYCLOAK_DOMAIN) -KEYCLOAK_URL= - -# --- Nexus --- -# Nom d'utilisateur admin Nexus -NEXUS_ADMIN= -# Mot de passe admin Nexus -NEXUS_ADMIN_PASSWORD= -# URL de l'instance Nexus -NEXUS_URL= -# URL interne de l'API Nexus -NEXUS_INTERNAL_URL= - -# --- Divers --- -# Répertoire racine des projets dans Gitlab -PROJECTS_ROOT_DIR=forge-mi/projects - -# --- SonarQube --- -# Token d'API SonarQube -SONAR_API_TOKEN= -# URL de l'instance SonarQube -SONARQUBE_URL= -# URL interne de l'API SonarQube -SONARQUBE_INTERNAL_URL= - -# --- Vault --- -# Token d'authentification Vault -VAULT_TOKEN= -# URL de l'instance Vault -VAULT_URL= -# URL interne de l'API Vault -VAULT_INTERNAL_URL= - -# --- Kubeconfig --- -# Chemin vers le kubeconfig sur la machine hôte (monté en volume dans Docker) -KUBECONFIG_HOST_PATH= -# Chemin vers le kubeconfig à l'intérieur du conteneur -KUBECONFIG_PATH=$HOME/.kube/config -# Contexte Kubernetes à utiliser -KUBECONFIG_CTX= - -# Répertoire contenant les plugins externes (monté en volume dans Docker) -EXTERNAL_PLUGINS_DIR_HOST_PATH=/path/to/plugins - -# --- Configuration Observability (Grafana) --- -# Activer le plugin observabilité -USE_OBSERVABILITY=false -# URL publique de Grafana -GRAFANA_URL= -# Version du chart Helm dso-observability -DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7 - -# --- Configuration proxy (si nécessaire pour accéder aux services externes) --- -# HTTP_PROXY= -# HTTPS_PROXY= -# NO_PROXY= - -# Configuration OpenCDS -OPENCDS_URL= -OPENCDS_INTERNAL_URL= -OPENCDS_API_TOKEN=token -OPENCDS_API_TLS_REJECT_UNAUTHORIZED=false diff --git a/apps/server-nestjs/prisma.config.ts b/apps/server-nestjs/prisma.config.ts index ff5aaf9186..057121c97b 100644 --- a/apps/server-nestjs/prisma.config.ts +++ b/apps/server-nestjs/prisma.config.ts @@ -1,20 +1,6 @@ -import fs from 'node:fs' import path from 'node:path' -import { parseEnv } from 'node:util' import { defineConfig } from 'prisma/config' -if (process.env.DOCKER !== 'true' && fs.existsSync('.env')) { - Object.assign(process.env, Object.fromEntries( - Object.entries(parseEnv(fs.readFileSync(path.resolve('.env'), 'utf-8'))).filter(([_k, v]) => Boolean(v)), - )) -} - -if (process.env.INTEGRATION === 'true' && fs.existsSync('.env.integ')) { - Object.assign(process.env, Object.fromEntries( - Object.entries(parseEnv(fs.readFileSync(path.resolve('.env.integ'), 'utf-8'))).filter(([_k, v]) => Boolean(v)), - )) -} - export default defineConfig({ schema: path.join('src', 'prisma', 'schema'), migrations: { diff --git a/apps/server-nestjs/src/main.module.ts b/apps/server-nestjs/src/main.module.ts index 362ea2d114..92b26c8d28 100644 --- a/apps/server-nestjs/src/main.module.ts +++ b/apps/server-nestjs/src/main.module.ts @@ -26,12 +26,10 @@ import { SystemSettingsModule } from './modules/system-settings/system-settings. import { UserModule } from './modules/user/user.module' import { VersionModule } from './modules/version/version.module' import { ZoneModule } from './modules/zone/zone.module' -import { getDotenvPaths } from './utils/dotenv.utils' @Module({ imports: [ ConfigModule.forRoot({ - envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory], }), diff --git a/apps/server-nestjs/src/utils/dotenv.utils.spec.ts b/apps/server-nestjs/src/utils/dotenv.utils.spec.ts deleted file mode 100644 index b5c7014c46..0000000000 --- a/apps/server-nestjs/src/utils/dotenv.utils.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { getDotenvPaths, getExistingDotenvPaths } from './dotenv.utils' - -describe('dotenv.utils', () => { - afterEach(() => vi.unstubAllEnvs()) - - describe('getDotenvPaths', () => { - it('returns [".env"] when no env vars set', () => { - expect(getDotenvPaths()).toEqual(['.env']) - }) - - it('returns [".env", ".env.integ"] when INTEGRATION=true', () => { - vi.stubEnv('INTEGRATION', 'true') - expect(getDotenvPaths()).toEqual(['.env', '.env.integ']) - }) - - it('returns [".env.docker"] when DOCKER=true', () => { - vi.stubEnv('DOCKER', 'true') - expect(getDotenvPaths()).toEqual(['.env.docker']) - }) - - it('returns [".env.integ", ".env.docker"] when DOCKER=true and INTEGRATION=true', () => { - vi.stubEnv('DOCKER', 'true') - vi.stubEnv('INTEGRATION', 'true') - expect(getDotenvPaths()).toEqual(['.env.integ', '.env.docker']) - }) - }) - - describe('getExistingDotenvPaths', () => { - it('filters to only existing files', () => { - const paths = getExistingDotenvPaths() - expect(paths.every(p => typeof p === 'string')).toBe(true) - }) - }) -}) diff --git a/apps/server-nestjs/src/utils/dotenv.utils.ts b/apps/server-nestjs/src/utils/dotenv.utils.ts deleted file mode 100644 index 7d3e0a0501..0000000000 --- a/apps/server-nestjs/src/utils/dotenv.utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import fs from 'node:fs' - -export function getDotenvPaths(): string[] { - const paths: string[] = [] - - // Load .env unless DOCKER=true - if (process.env.DOCKER !== 'true') { - paths.push('.env') - } - - // Load .env.integ if INTEGRATION=true - if (process.env.INTEGRATION === 'true') { - paths.push('.env.integ') - } - - // Load .env.docker if DOCKER=true - if (process.env.DOCKER === 'true') { - paths.push('.env.docker') - } - - return paths -} - -export function getExistingDotenvPaths(): string[] { - return getDotenvPaths().filter(path => fs.existsSync(path)) -} diff --git a/apps/server-nestjs/test/argocd.e2e-spec.ts b/apps/server-nestjs/test/argocd.e2e-spec.ts index 107d665d2b..42283f0f9c 100644 --- a/apps/server-nestjs/test/argocd.e2e-spec.ts +++ b/apps/server-nestjs/test/argocd.e2e-spec.ts @@ -19,7 +19,6 @@ import { EventsModule } from '../src/modules/infrastructure/events/events.module import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module' import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { VaultClientService } from '../src/modules/vault/vault-client.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { ARGOCD_RECONCILE_TIMEOUT, GITLAB_SYNC_TIMEOUT } from './constants' const canRunArgoCDE2E @@ -57,7 +56,7 @@ describeWithArgoCD('ArgoCDService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ArgoCDModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [ArgoCDModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/gitlab.e2e-spec.ts b/apps/server-nestjs/test/gitlab.e2e-spec.ts index 2c1c2c501e..73e0f344cc 100644 --- a/apps/server-nestjs/test/gitlab.e2e-spec.ts +++ b/apps/server-nestjs/test/gitlab.e2e-spec.ts @@ -19,7 +19,6 @@ import { EventsModule } from '../src/modules/infrastructure/events/events.module import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module' import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { VaultClientService } from '../src/modules/vault/vault-client.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { getAll } from '../src/utils/iterable.utils' import { GITLAB_PURGE_SYNC_TIMEOUT, GITLAB_SYNC_TIMEOUT } from './constants' @@ -44,7 +43,7 @@ describeWithGitLab('GitlabService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [GitlabModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [GitlabModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/keycloak.e2e-spec.ts b/apps/server-nestjs/test/keycloak.e2e-spec.ts index 5abcb02576..d96d881aad 100644 --- a/apps/server-nestjs/test/keycloak.e2e-spec.ts +++ b/apps/server-nestjs/test/keycloak.e2e-spec.ts @@ -17,7 +17,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { KEYCLOAK_ADMIN_CLIENT, KeycloakClientService } from '../src/modules/keycloak/keycloak-client.service' import { projectSelect } from '../src/modules/keycloak/keycloak-datastore.service' import { KeycloakModule } from '../src/modules/keycloak/keycloak.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { KEYCLOAK_GROUP_SYNC_TIMEOUT } from './constants' const canRunKeycloakE2E @@ -40,7 +39,7 @@ describeWithKeycloak('KeycloakService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [KeycloakModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [KeycloakModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/log.e2e-spec.ts b/apps/server-nestjs/test/log.e2e-spec.ts index 4d81a4e725..bfc1eff161 100644 --- a/apps/server-nestjs/test/log.e2e-spec.ts +++ b/apps/server-nestjs/test/log.e2e-spec.ts @@ -12,7 +12,6 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { LogModule } from '../src/modules/log/log.module' import { LogService } from '../src/modules/log/log.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunLogE2E = Boolean(process.env.E2E) @@ -32,7 +31,7 @@ describeWithLog('LogService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), LogModule, AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), LogModule, AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/nexus.e2e-spec.ts b/apps/server-nestjs/test/nexus.e2e-spec.ts index 3c0d31f7ef..03a4a57e2d 100644 --- a/apps/server-nestjs/test/nexus.e2e-spec.ts +++ b/apps/server-nestjs/test/nexus.e2e-spec.ts @@ -21,7 +21,6 @@ import { NexusModule } from '../src/modules/nexus/nexus.module' import { generateNexusCredPath } from '../src/modules/nexus/nexus.utils' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { NEXUS_SYNC_TIMEOUT } from './constants' const canRunNexusE2E @@ -43,7 +42,7 @@ describeWithNexus('NexusService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [NexusModule, VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [NexusModule, VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/observability.e2e-spec.ts b/apps/server-nestjs/test/observability.e2e-spec.ts index 4d79f6ee85..3c5e617f90 100644 --- a/apps/server-nestjs/test/observability.e2e-spec.ts +++ b/apps/server-nestjs/test/observability.e2e-spec.ts @@ -27,7 +27,6 @@ import { GRAFANA_SUBGROUP_PROD_RW, } from '../src/modules/observability/observability.constants' import { ObservabilityModule } from '../src/modules/observability/observability.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { KEYCLOAK_GROUP_SYNC_TIMEOUT } from './constants' const canRunObservabilityE2E @@ -88,7 +87,7 @@ describeWithObservability('ObservabilityService (e2e)', () => { } moduleRef = await Test.createTestingModule({ - imports: [ObservabilityModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [ObservabilityModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }) .overrideProvider(GitlabClientService) .useValue(gitlabStub) diff --git a/apps/server-nestjs/test/project-bulk.e2e-spec.ts b/apps/server-nestjs/test/project-bulk.e2e-spec.ts index 8548971e55..0886364b58 100644 --- a/apps/server-nestjs/test/project-bulk.e2e-spec.ts +++ b/apps/server-nestjs/test/project-bulk.e2e-spec.ts @@ -13,7 +13,6 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { ProjectBulkModule } from '../src/modules/project-bulk/project-bulk.module' import { ProjectBulkService } from '../src/modules/project-bulk/project-bulk.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectBulkE2E = Boolean(process.env.E2E) @@ -31,7 +30,7 @@ describeWithProjectBulk('ProjectBulkService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectBulkModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectBulkModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project-hooks.e2e-spec.ts b/apps/server-nestjs/test/project-hooks.e2e-spec.ts index 91d56e504f..b73d9f8d56 100644 --- a/apps/server-nestjs/test/project-hooks.e2e-spec.ts +++ b/apps/server-nestjs/test/project-hooks.e2e-spec.ts @@ -17,7 +17,6 @@ import { ProjectHooksModule } from '../src/modules/project-hooks/project-hooks.m import { ProjectHooksService } from '../src/modules/project-hooks/project-hooks.service' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultService } from '../src/modules/vault/vault.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectHooksE2E = Boolean(process.env.E2E) @@ -41,7 +40,7 @@ describeWithProjectHooks('ProjectHooksService (e2e)', () => { vaultClient = mockDeep() moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectHooksModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectHooksModule], providers: [ { provide: VaultService, useValue: vaultService }, { provide: VaultClientService, useValue: vaultClient }, diff --git a/apps/server-nestjs/test/project-members.e2e-spec.ts b/apps/server-nestjs/test/project-members.e2e-spec.ts index 315bdba2dd..47c592b1c3 100644 --- a/apps/server-nestjs/test/project-members.e2e-spec.ts +++ b/apps/server-nestjs/test/project-members.e2e-spec.ts @@ -17,7 +17,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { KeycloakClientService } from '../src/modules/keycloak/keycloak-client.service' import { ProjectMembersModule } from '../src/modules/project-members/project-members.module' import { ProjectMembersService } from '../src/modules/project-members/project-members.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectMembersE2E = Boolean(process.env.E2E) @@ -38,7 +37,7 @@ describeWithProjectMembers('ProjectMembersService (e2e)', () => { keycloakClient.getUserByEmail.mockResolvedValue(undefined) moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectMembersModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectMembersModule], providers: [ { provide: KeycloakClientService, useValue: keycloakClient }, ], diff --git a/apps/server-nestjs/test/project-roles.e2e-spec.ts b/apps/server-nestjs/test/project-roles.e2e-spec.ts index 3f180330d0..edaf97febe 100644 --- a/apps/server-nestjs/test/project-roles.e2e-spec.ts +++ b/apps/server-nestjs/test/project-roles.e2e-spec.ts @@ -14,7 +14,6 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { ProjectRolesModule } from '../src/modules/project-roles/project-roles.module' import { ProjectRolesService } from '../src/modules/project-roles/project-roles.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectRolesE2E = Boolean(process.env.E2E) @@ -32,7 +31,7 @@ describeWithProjectRoles('ProjectRolesService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectRolesModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectRolesModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project-secrets.e2e-spec.ts b/apps/server-nestjs/test/project-secrets.e2e-spec.ts index 037cb7e1e0..172c434e4d 100644 --- a/apps/server-nestjs/test/project-secrets.e2e-spec.ts +++ b/apps/server-nestjs/test/project-secrets.e2e-spec.ts @@ -15,7 +15,6 @@ import { ProjectSecretsModule } from '../src/modules/project-secrets/project-sec import { ProjectSecretsService } from '../src/modules/project-secrets/project-secrets.service' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { generateProjectPath } from '../src/modules/vault/vault.utils' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectSecretsE2E = Boolean(process.env.E2E) @@ -44,7 +43,7 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => { vi.stubEnv('USE_VAULT', 'true') moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectSecretsModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectSecretsModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project-services.e2e-spec.ts b/apps/server-nestjs/test/project-services.e2e-spec.ts index 4a8b41f222..4f3f2dd9c6 100644 --- a/apps/server-nestjs/test/project-services.e2e-spec.ts +++ b/apps/server-nestjs/test/project-services.e2e-spec.ts @@ -14,7 +14,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { NEXUS_CONFIG_KEY_ACTIVATE_NPM_REPO, PLUGIN_NAME } from '../src/modules/nexus/nexus.constants' import { ProjectServicesModule } from '../src/modules/project-services/project-services.module' import { ProjectServicesService } from '../src/modules/project-services/project-services.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunServicesE2E = Boolean(process.env.E2E) const describeWithServices = describe.runIf(canRunServicesE2E) @@ -29,7 +28,7 @@ describeWithServices('ProjectServicesService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectServicesModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectServicesModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project.e2e-spec.ts b/apps/server-nestjs/test/project.e2e-spec.ts index 0a670aa45a..eb28668c78 100644 --- a/apps/server-nestjs/test/project.e2e-spec.ts +++ b/apps/server-nestjs/test/project.e2e-spec.ts @@ -16,7 +16,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { makeCreateProjectBody } from '../src/modules/project/project-testing.utils' import { ProjectModule } from '../src/modules/project/project.module' import { ProjectService } from '../src/modules/project/project.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectE2E = Boolean(process.env.E2E) @@ -32,7 +31,7 @@ describeWithProject('ProjectService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/registry.e2e-spec.ts b/apps/server-nestjs/test/registry.e2e-spec.ts index 5f0b982883..38f9315a2c 100644 --- a/apps/server-nestjs/test/registry.e2e-spec.ts +++ b/apps/server-nestjs/test/registry.e2e-spec.ts @@ -16,7 +16,6 @@ import { RegistryModule } from '../src/modules/registry/registry.module' import { RegistryService } from '../src/modules/registry/registry.service' import { getHostFromUrl, getProjectVaultPath } from '../src/modules/registry/registry.utils' import { VaultClientService } from '../src/modules/vault/vault-client.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { getAll } from '../src/utils/iterable.utils' const canRunRegistryE2E @@ -36,7 +35,7 @@ describeWithRegistry('RegistryService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory, harborConfigFactory] }), RegistryModule, EventsModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory, harborConfigFactory] }), RegistryModule, EventsModule], }) .compile() diff --git a/apps/server-nestjs/test/sonarqube.e2e-spec.ts b/apps/server-nestjs/test/sonarqube.e2e-spec.ts index ac301a8a53..eef4760914 100644 --- a/apps/server-nestjs/test/sonarqube.e2e-spec.ts +++ b/apps/server-nestjs/test/sonarqube.e2e-spec.ts @@ -23,7 +23,6 @@ import { SonarqubeModule } from '../src/modules/sonarqube/sonarqube.module' import { SonarqubeService } from '../src/modules/sonarqube/sonarqube.service' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { getAll } from '../src/utils/iterable.utils' import { SONARQUBE_PROJECT_TIMEOUT } from './constants' @@ -49,7 +48,7 @@ describeWithSonarqube('SonarqubeService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [SonarqubeModule, GitlabModule, VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory, gitlabConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [SonarqubeModule, GitlabModule, VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory, gitlabConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/vault.e2e-spec.ts b/apps/server-nestjs/test/vault.e2e-spec.ts index 8404cbcdcf..010dec4e72 100644 --- a/apps/server-nestjs/test/vault.e2e-spec.ts +++ b/apps/server-nestjs/test/vault.e2e-spec.ts @@ -15,7 +15,6 @@ import { VaultClientService } from '../src/modules/vault/vault-client.service' import { projectSelect } from '../src/modules/vault/vault-datastore.service' import { makeProjectWithDetails } from '../src/modules/vault/vault-testing.utils' import { VaultModule } from '../src/modules/vault/vault.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { VAULT_PROVISION_TIMEOUT } from './constants' const canRunVaultE2E @@ -35,7 +34,7 @@ describeWithVault('VaultService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/zone.e2e-spec.ts b/apps/server-nestjs/test/zone.e2e-spec.ts index f13978f7f1..8b6b8fcc41 100644 --- a/apps/server-nestjs/test/zone.e2e-spec.ts +++ b/apps/server-nestjs/test/zone.e2e-spec.ts @@ -14,7 +14,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' import { VaultService } from '../src/modules/vault/vault.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { VAULT_PROVISION_TIMEOUT } from './constants' const canRunZoneE2E = Boolean(process.env.E2E) @@ -33,7 +32,7 @@ describeWithZone('Zone lifecycle (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/mise.toml b/mise.toml index 60fdae870f..198efae122 100644 --- a/mise.toml +++ b/mise.toml @@ -36,6 +36,13 @@ run = "docker compose -f docker/docker-compose.local.yml up -d --remove-orphans" [tasks."server-nestjs:dev"] run = "SERVER_HOST=$NESTJS_SERVER_HOST SERVER_PORT=$NESTJS_SERVER_PORT KEYCLOAK_PROTOCOL=$NESTJS_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$NESTJS_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$NESTJS_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$NESTJS_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$NESTJS_KEYCLOAK_REDIRECT_URI CONTACT_EMAIL=$NESTJS_CONTACT_EMAIL fnox exec -- pnpm --filter server-nestjs run start:dev" +[tasks."server-nestjs:test"] +run = "mise -E ci run server-nestjs:test-inner" + +[tasks."server-nestjs:test-inner"] +hide = true +run = "NODE_ENV=test SERVER_HOST=$NESTJS_SERVER_HOST SERVER_PORT=$NESTJS_SERVER_PORT KEYCLOAK_PROTOCOL=$NESTJS_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$NESTJS_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$NESTJS_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$NESTJS_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$NESTJS_KEYCLOAK_REDIRECT_URI CONTACT_EMAIL=$NESTJS_CONTACT_EMAIL fnox exec -- pnpm --filter server-nestjs test" + [tasks."client:dev"] run = "SERVER_HOST=$CLIENT_SERVER_HOST SERVER_PORT=$CLIENT_SERVER_PORT CLIENT_PORT=$CLIENT_PORT KEYCLOAK_PROTOCOL=$CLIENT_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$CLIENT_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$CLIENT_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$CLIENT_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$CLIENT_KEYCLOAK_REDIRECT_URI OPENCDS_ENABLED=$CLIENT_OPENCDS_ENABLED CONTACT_EMAIL=$CLIENT_CONTACT_EMAIL pnpm --filter client run dev" From a82587b55430cd7e8260463fa161a0c7618f6e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 28 Sep 2026 17:48:16 +0200 Subject: [PATCH 04/14] refactor(client): remove dotenv configuration Whitelist browser-visible values and keep runtime substitution secret-free. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- apps/client/.env-example | 24 ------------------------ apps/client/.env.docker-example | 21 --------------------- apps/client/.env.integ-example | 11 ----------- apps/client/nginx/entrypoint.sh | 2 ++ apps/client/vite.config.ts | 23 +++++++++++++++-------- mise.toml | 7 +++++++ 6 files changed, 24 insertions(+), 64 deletions(-) delete mode 100644 apps/client/.env-example delete mode 100644 apps/client/.env.docker-example delete mode 100644 apps/client/.env.integ-example diff --git a/apps/client/.env-example b/apps/client/.env-example deleted file mode 100644 index 2f23f06f97..0000000000 --- a/apps/client/.env-example +++ /dev/null @@ -1,24 +0,0 @@ -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# Hôte du serveur backend vers lequel le client proxifie les appels API -SERVER_HOST=localhost -# Port d'écoute du serveur backend -# En développement local avec le nginx-strangler (pnpm dev -# + docker-compose.local.yml), il vaut mieux mettre SERVER_PORT=4000 pour que -# le proxy Vite passe par nginx-strangler plutôt que directement par server (4001) -# ou server-nestjs (3001). -SERVER_PORT=4000 -# Domaine (host:port) du serveur Keycloak pour l'authentification -KEYCLOAK_DOMAIN=localhost:8090 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http | https) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté frontend -KEYCLOAK_CLIENT_ID=dso-console-frontend -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# Active/désactive le module OpenCDS (true | false) -OPENCDS_ENABLED=false -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr diff --git a/apps/client/.env.docker-example b/apps/client/.env.docker-example deleted file mode 100644 index 1f05a462c6..0000000000 --- a/apps/client/.env.docker-example +++ /dev/null @@ -1,21 +0,0 @@ -# Spécifique à ce ficher. Permet de dire que la stack est conteneurisée -DOCKER=true -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# Hôte du nginx vers lequel le client proxifie les appels API -SERVER_HOST=nginx-strangler -SERVER_PORT=8080 -# Domaine (host:port) du serveur Keycloak pour l'authentification -KEYCLOAK_DOMAIN=localhost:8090 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http | https) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté frontend -KEYCLOAK_CLIENT_ID=dso-console-frontend -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# Active/désactive le module OpenCDS (true | false) -OPENCDS_ENABLED=false -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr diff --git a/apps/client/.env.integ-example b/apps/client/.env.integ-example deleted file mode 100644 index 4f870dfd2f..0000000000 --- a/apps/client/.env.integ-example +++ /dev/null @@ -1,11 +0,0 @@ -# Surcharges pour le branchement à un environnement d'intégration distant. -# Ces valeurs écrasent celles de .env et .env.docker quand INTEGRATION=true. - -# Domaine du Keycloak de l'environnement d'intégration (ex: keycloak.dso.example.fr) -KEYCLOAK_DOMAIN= -# Royaume Keycloak de l'environnement d'intégration -KEYCLOAK_REALM= -# En intégration, Keycloak est généralement exposé en HTTPS -KEYCLOAK_PROTOCOL=https -# Identifiant du client Keycloak frontend sur l'environnement d'intégration -KEYCLOAK_CLIENT_ID= diff --git a/apps/client/nginx/entrypoint.sh b/apps/client/nginx/entrypoint.sh index 98dfe3bcd9..e52d3198ae 100755 --- a/apps/client/nginx/entrypoint.sh +++ b/apps/client/nginx/entrypoint.sh @@ -11,9 +11,11 @@ make_envsubst_vars() { printf '%s\n' "$fmt" } +# Browser-visible configuration only; never add a secret here. ENV_VARS=$(make_envsubst_vars \ SERVER_HOST \ SERVER_PORT \ + CLIENT_PORT \ OPENCDS_ENABLED \ KEYCLOAK_PROTOCOL \ KEYCLOAK_DOMAIN \ diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index 817f1306c6..42ff334a6c 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs' import { fileURLToPath, URL } from 'node:url' import { vueDsfrAutoimportPreset, @@ -11,19 +10,27 @@ import Components from 'unplugin-vue-components/vite' import { defineConfig } from 'vite' import { VitePWA } from 'vite-plugin-pwa' -if (process.env.INTEGRATION === 'true' && fs.existsSync('.env.integ')) - process.loadEnvFile('.env.integ') - -if (process.env.DOCKER !== 'true' && fs.existsSync('.env')) - process.loadEnvFile('.env') - const serverHost = process.env.SERVER_HOST ?? 'localhost' const serverPort = process.env.SERVER_PORT ?? 4000 const clientPort = process.env.CLIENT_PORT ?? 8080 +const publicClientEnv = { + NODE_ENV: process.env.NODE_ENV, + SERVER_HOST: process.env.SERVER_HOST, + SERVER_PORT: process.env.SERVER_PORT, + CLIENT_PORT: process.env.CLIENT_PORT, + KEYCLOAK_PROTOCOL: process.env.KEYCLOAK_PROTOCOL, + KEYCLOAK_DOMAIN: process.env.KEYCLOAK_DOMAIN, + KEYCLOAK_REALM: process.env.KEYCLOAK_REALM, + KEYCLOAK_CLIENT_ID: process.env.KEYCLOAK_CLIENT_ID, + KEYCLOAK_REDIRECT_URI: process.env.KEYCLOAK_REDIRECT_URI, + OPENCDS_ENABLED: process.env.OPENCDS_ENABLED, + CONTACT_EMAIL: process.env.CONTACT_EMAIL, +} + const define = process.env.NODE_ENV === 'production' ? { 'process.env': { APP_VERSION: process.env.APP_VERSION } } - : { 'process.env': process.env } + : { 'process.env': publicClientEnv } // https://vitejs.dev/config/ export default defineConfig({ diff --git a/mise.toml b/mise.toml index 198efae122..395539767a 100644 --- a/mise.toml +++ b/mise.toml @@ -46,6 +46,13 @@ run = "NODE_ENV=test SERVER_HOST=$NESTJS_SERVER_HOST SERVER_PORT=$NESTJS_SERVER_ [tasks."client:dev"] run = "SERVER_HOST=$CLIENT_SERVER_HOST SERVER_PORT=$CLIENT_SERVER_PORT CLIENT_PORT=$CLIENT_PORT KEYCLOAK_PROTOCOL=$CLIENT_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$CLIENT_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$CLIENT_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$CLIENT_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$CLIENT_KEYCLOAK_REDIRECT_URI OPENCDS_ENABLED=$CLIENT_OPENCDS_ENABLED CONTACT_EMAIL=$CLIENT_CONTACT_EMAIL pnpm --filter client run dev" +[tasks."client:build"] +run = "mise -E ci run client:build-inner" + +[tasks."client:build-inner"] +hide = true +run = "APP_VERSION=${APP_VERSION:-dev} pnpm --filter client run build" + [tasks.lint] run = "pnpm lint" From b31abcf68230c2da330b9322c6d7a8782bb23c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 29 Sep 2026 15:12:24 +0200 Subject: [PATCH 05/14] feat: prepare scoped Docker and CI configuration Add scoped NestJS public configuration and reject non-legacy tracked env files. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- ci/scripts/check-environment-files.sh | 9 +++++++++ docs/environment-variables.md | 2 +- mise.ci.toml | 3 +++ mise.docker.toml | 3 +++ mise.toml | 18 ++++++++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100755 ci/scripts/check-environment-files.sh diff --git a/ci/scripts/check-environment-files.sh b/ci/scripts/check-environment-files.sh new file mode 100755 index 0000000000..c838eb477d --- /dev/null +++ b/ci/scripts/check-environment-files.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +unexpected="$(git ls-files | grep -E '(^|/)\.env[^/]*$' | grep -v '^apps/server/\.env' || true)" +if [ -n "$unexpected" ]; then + printf 'Unexpected tracked environment files outside apps/server:\n%s\n' "$unexpected" >&2 + exit 1 +fi diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 1263f9ef2b..ec5bf337e9 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -1,6 +1,6 @@ # Registre des variables d’environnement -Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. +Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. Les noms du tableau sont les contrats des processus ; `mise` préfixe les valeurs propres à un consommateur (`CLIENT_*` ou `NESTJS_*`) et les tâches ou Compose les traduisent explicitement afin d’éviter les collisions frontend/backend. | Classe | Variables | Source | Consommateurs | | --- | --- | --- | --- | diff --git a/mise.ci.toml b/mise.ci.toml index e11a3909b5..c4b1a32553 100644 --- a/mise.ci.toml +++ b/mise.ci.toml @@ -9,3 +9,6 @@ CLIENT_OPENCDS_ENABLED = "true" NESTJS_SERVER_HOST = "0.0.0.0" NESTJS_SERVER_PORT = "3001" NESTJS_KEYCLOAK_DOMAIN = "keycloak:8080" +NESTJS_USE_SERVICE_CHAIN = "true" +NESTJS_OPENCDS_URL = "http://dso-console_opencds-mockoon:3100/api/v1" +NESTJS_OPENCDS_INTERNAL_URL = "http://dso-console_opencds-mockoon:3100/api/v1/health" diff --git a/mise.docker.toml b/mise.docker.toml index 20ca697525..7f51151a76 100644 --- a/mise.docker.toml +++ b/mise.docker.toml @@ -6,3 +6,6 @@ CLIENT_OPENCDS_ENABLED = "true" NESTJS_SERVER_HOST = "0.0.0.0" NESTJS_SERVER_PORT = "3001" NESTJS_KEYCLOAK_DOMAIN = "keycloak:8080" +NESTJS_USE_SERVICE_CHAIN = "true" +NESTJS_OPENCDS_URL = "http://dso-console_opencds-mockoon:3100/api/v1" +NESTJS_OPENCDS_INTERNAL_URL = "http://dso-console_opencds-mockoon:3100/api/v1/health" diff --git a/mise.toml b/mise.toml index 395539767a..8c2e89b349 100644 --- a/mise.toml +++ b/mise.toml @@ -26,6 +26,24 @@ NESTJS_KEYCLOAK_REALM = "dso" NESTJS_KEYCLOAK_CLIENT_ID = "dso-console-backend" NESTJS_KEYCLOAK_REDIRECT_URI = "http://localhost:8080" NESTJS_CONTACT_EMAIL = "cloudpinative-relations@interieur.gouv.fr" +NESTJS_PROJECTS_ROOT_DIR = "" +NESTJS_KEYCLOAK_ADMIN_CLIENT_ID = "admin-cli" +NESTJS_KEYCLOAK_JWKS_CACHE_TTL_MS = "300000" +NESTJS_KEYCLOAK_JWKS_TIMEOUT_MS = "1000" +NESTJS_KEYCLOAK_OPENID_CONFIGURATION_CACHE_TTL_MS = "300000" +NESTJS_USE_ARGOCD = "false" +NESTJS_USE_GITLAB = "false" +NESTJS_USE_HARBOR = "false" +NESTJS_USE_NEXUS = "false" +NESTJS_USE_OBSERVABILITY = "false" +NESTJS_USE_SERVICE_CHAIN = "false" +NESTJS_USE_SONARQUBE = "false" +NESTJS_USE_VAULT = "false" +NESTJS_OPENCDS_URL = "" +NESTJS_OPENCDS_INTERNAL_URL = "" +NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED = "false" +NESTJS_GRAFANA_URL = "https://grafana.example.com" +NESTJS_DSO_OBSERVABILITY_CHART_VERSION = "dso-observability-0.1.7" [tasks.setup] run = "./ci/scripts/init-env.sh && ./scripts/bootstrap-secrets.sh" From d60e9478fc826e87abdcbc78132fc1f472164ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 29 Sep 2026 15:15:02 +0200 Subject: [PATCH 06/14] ci: run checks through mise Use pinned mise tooling for lint, unit tests, and Playwright CI commands. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- .github/workflows/job-lint.yml | 20 +++++++++----------- .github/workflows/job-playwright.yml | 9 ++++++++- .github/workflows/job-tests-unit.yml | 16 ++++++---------- ENVIRONMENTS.md | 12 ++++++++++++ mise.toml | 3 +++ 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.github/workflows/job-lint.yml b/.github/workflows/job-lint.yml index bbbae077d6..0f8175ce52 100644 --- a/.github/workflows/job-lint.yml +++ b/.github/workflows/job-lint.yml @@ -24,16 +24,12 @@ jobs: - name: Checks-out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "${{ inputs.NODE_VERSION }}" + version: 2026.5.15 + install: true + cache: true - name: Get pnpm store directory id: pnpm-store @@ -53,8 +49,10 @@ jobs: run: pnpm install --frozen-lockfile - name: Check lint error - run: | - ./ci/scripts/run-tests.sh -l + run: mise -E ci run lint + + - name: Check environment-file boundary + run: ./ci/scripts/check-environment-files.sh - name: Validate nginx-strangler config run: | diff --git a/.github/workflows/job-playwright.yml b/.github/workflows/job-playwright.yml index 8b3ac0a3df..6b882f0ae1 100644 --- a/.github/workflows/job-playwright.yml +++ b/.github/workflows/job-playwright.yml @@ -37,6 +37,13 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.5.15 + install: true + cache: true + - name: Ensure Playwright browsers run: pnpm --dir playwright exec playwright install chromium firefox @@ -53,7 +60,7 @@ jobs: # See https://playwright.dev/docs/test-sharding for how Playwright handles test sharding - name: Run Playwright tests - run: pnpm --dir playwright exec playwright test --grep @e2e --shard=${{ matrix.shard }}/4 --reporter=blob + run: mise -E ci run playwright:test -- --shard=${{ matrix.shard }}/4 --reporter=blob - name: Dump docker compose state on failure if: ${{ failure() }} diff --git a/.github/workflows/job-tests-unit.yml b/.github/workflows/job-tests-unit.yml index 993ff302cf..1d45a1efdb 100644 --- a/.github/workflows/job-tests-unit.yml +++ b/.github/workflows/job-tests-unit.yml @@ -31,16 +31,12 @@ jobs: - name: Checks-out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "${{ inputs.NODE_VERSION }}" + version: 2026.5.15 + install: true + cache: true - name: Get pnpm store directory id: pnpm-store @@ -68,7 +64,7 @@ jobs: - name: Run unit tests run: | ./ci/scripts/init-env.sh - ./ci/scripts/run-tests.sh -u + mise -E ci run test:unit - name: Upload vitest coverage artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 diff --git a/ENVIRONMENTS.md b/ENVIRONMENTS.md index 35b50ed30c..85a2428125 100644 --- a/ENVIRONMENTS.md +++ b/ENVIRONMENTS.md @@ -61,6 +61,18 @@ Le client et `server-nestjs` migrent vers `mise` pour la configuration publique La décision et le registre de variables sont documentés dans [`docs/adr/0001-mise-fnox-environment-management.md`](docs/adr/0001-mise-fnox-environment-management.md) et [`docs/environment-variables.md`](docs/environment-variables.md). +| Cas | Commande | +| --- | --- | +| Initialisation locale | `mise run setup` | +| Infrastructure locale | `mise run dev` | +| NestJS local | `mise run server-nestjs:dev` | +| Client local | `mise run client:dev` | +| Stack Docker locale | `mise run docker:dev` | +| Intégration hybride | `mise run integ` | +| CI | `mise -E ci run ` | + +`apps/server` reste la seule exception transitoire à ces commandes, jusqu'à sa suppression. + ## Configuration pour le développement entièrement en local Le développement entièrement en local suit cette organisation des composants: diff --git a/mise.toml b/mise.toml index 8c2e89b349..89cc2c1528 100644 --- a/mise.toml +++ b/mise.toml @@ -77,6 +77,9 @@ run = "pnpm lint" [tasks."test:unit"] run = "fnox exec -- pnpm test" +[tasks."playwright:test"] +run = "fnox -P ci exec -- pnpm --dir playwright exec playwright test --grep @e2e" + [tasks."docker:dev"] run = "mise -E docker run docker:dev-inner" From e5c56fc30c06c3e5065168bd4f56913c21cb44f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 29 Sep 2026 15:16:31 +0200 Subject: [PATCH 07/14] refactor(docker): inject non-legacy environment explicitly Map client and NestJS configuration from scoped mise and fnox inputs. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- docker/docker-compose.ci.yml | 42 ++++++++++++++++++++++++--------- docker/docker-compose.dev.yml | 35 +++++++++++++++++++++++---- docker/docker-compose.integ.yml | 37 ++++++++++++++++++++++++----- 3 files changed, 93 insertions(+), 21 deletions(-) diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index 10ce41a23b..04a7e8ee59 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -86,15 +86,28 @@ services: condition: service_healthy opencds-mockoon: condition: service_healthy - env_file: - - ../apps/server-nestjs/.env.docker environment: - NODE_ENV: 'production' - CI: 'true' - DEV_SETUP: 'true' - OPENCDS_URL: 'http://dso-console_opencds-mockoon:3100/api/v1' - OPENCDS_API_TOKEN: 'token' - OPENCDS_API_TLS_REJECT_UNAUTHORIZED: 'false' + NODE_ENV: ${NODE_ENV:?NODE_ENV is required} + CI: ${CI:-false} + DEV_SETUP: ${DEV_SETUP:-false} + SERVER_HOST: ${NESTJS_SERVER_HOST:?NESTJS_SERVER_HOST is required} + SERVER_PORT: ${NESTJS_SERVER_PORT:?NESTJS_SERVER_PORT is required} + DB_URL: ${DB_URL:?DB_URL is required} + SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} + KEYCLOAK_PROTOCOL: ${NESTJS_KEYCLOAK_PROTOCOL:?NESTJS_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${NESTJS_KEYCLOAK_DOMAIN:?NESTJS_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${NESTJS_KEYCLOAK_REALM:?NESTJS_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${NESTJS_KEYCLOAK_CLIENT_ID:?NESTJS_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:?KEYCLOAK_CLIENT_SECRET is required} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:?KEYCLOAK_ADMIN is required} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} + KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} + CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} + OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} + OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} + OPENCDS_API_TLS_REJECT_UNAUTHORIZED: ${NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED:-true} networks: - dso-network @@ -131,10 +144,17 @@ services: user: root volumes: - ../apps/client/nginx/default.docker.conf:/etc/nginx/conf.d/default.conf - env_file: - - ../apps/client/.env.docker environment: - OPENCDS_ENABLED: 'true' + SERVER_HOST: ${CLIENT_SERVER_HOST:?CLIENT_SERVER_HOST is required} + SERVER_PORT: ${CLIENT_SERVER_PORT:?CLIENT_SERVER_PORT is required} + CLIENT_PORT: ${CLIENT_PORT:-8080} + KEYCLOAK_PROTOCOL: ${CLIENT_KEYCLOAK_PROTOCOL:?CLIENT_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${CLIENT_KEYCLOAK_DOMAIN:?CLIENT_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${CLIENT_KEYCLOAK_REALM:?CLIENT_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${CLIENT_KEYCLOAK_CLIENT_ID:?CLIENT_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_REDIRECT_URI: ${CLIENT_KEYCLOAK_REDIRECT_URI:?CLIENT_KEYCLOAK_REDIRECT_URI is required} + OPENCDS_ENABLED: ${CLIENT_OPENCDS_ENABLED:-false} + CONTACT_EMAIL: ${CLIENT_CONTACT_EMAIL:?CLIENT_CONTACT_EMAIL is required} networks: - dso-network diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index f09196aaf0..e1e9ea8a90 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -134,8 +134,26 @@ services: condition: service_healthy opencds-mockoon: condition: service_healthy - env_file: - - ../apps/server-nestjs/.env.docker + environment: + NODE_ENV: ${NODE_ENV:?NODE_ENV is required} + SERVER_HOST: ${NESTJS_SERVER_HOST:?NESTJS_SERVER_HOST is required} + SERVER_PORT: ${NESTJS_SERVER_PORT:?NESTJS_SERVER_PORT is required} + DB_URL: ${DB_URL:?DB_URL is required} + SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} + KEYCLOAK_PROTOCOL: ${NESTJS_KEYCLOAK_PROTOCOL:?NESTJS_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${NESTJS_KEYCLOAK_DOMAIN:?NESTJS_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${NESTJS_KEYCLOAK_REALM:?NESTJS_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${NESTJS_KEYCLOAK_CLIENT_ID:?NESTJS_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:?KEYCLOAK_CLIENT_SECRET is required} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:?KEYCLOAK_ADMIN is required} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} + KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} + CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} + OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} + OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} + OPENCDS_API_TLS_REJECT_UNAUTHORIZED: ${NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED:-true} develop: watch: - action: sync @@ -202,8 +220,17 @@ services: condition: service_started ports: - 8080:8080 - env_file: - - ../apps/client/.env.docker + environment: + SERVER_HOST: ${CLIENT_SERVER_HOST:?CLIENT_SERVER_HOST is required} + SERVER_PORT: ${CLIENT_SERVER_PORT:?CLIENT_SERVER_PORT is required} + CLIENT_PORT: ${CLIENT_PORT:-8080} + KEYCLOAK_PROTOCOL: ${CLIENT_KEYCLOAK_PROTOCOL:?CLIENT_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${CLIENT_KEYCLOAK_DOMAIN:?CLIENT_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${CLIENT_KEYCLOAK_REALM:?CLIENT_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${CLIENT_KEYCLOAK_CLIENT_ID:?CLIENT_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_REDIRECT_URI: ${CLIENT_KEYCLOAK_REDIRECT_URI:?CLIENT_KEYCLOAK_REDIRECT_URI is required} + OPENCDS_ENABLED: ${CLIENT_OPENCDS_ENABLED:-false} + CONTACT_EMAIL: ${CLIENT_CONTACT_EMAIL:?CLIENT_CONTACT_EMAIL is required} develop: watch: - action: sync diff --git a/docker/docker-compose.integ.yml b/docker/docker-compose.integ.yml index d023ad3698..123d6860c4 100644 --- a/docker/docker-compose.integ.yml +++ b/docker/docker-compose.integ.yml @@ -110,9 +110,26 @@ services: volumes: - ${KUBECONFIG_HOST_PATH}:${KUBECONFIG_PATH:-/home/node/.kube/config} - ${EXTERNAL_PLUGINS_DIR_HOST_PATH}:/plugins - env_file: - - ../apps/server-nestjs/.env.docker - - ../apps/server-nestjs/.env.integ + environment: + NODE_ENV: ${NODE_ENV:?NODE_ENV is required} + SERVER_HOST: ${NESTJS_SERVER_HOST:?NESTJS_SERVER_HOST is required} + SERVER_PORT: ${NESTJS_SERVER_PORT:?NESTJS_SERVER_PORT is required} + DB_URL: ${DB_URL:?DB_URL is required} + SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} + KEYCLOAK_PROTOCOL: ${NESTJS_KEYCLOAK_PROTOCOL:?NESTJS_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${NESTJS_KEYCLOAK_DOMAIN:?NESTJS_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${NESTJS_KEYCLOAK_REALM:?NESTJS_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${NESTJS_KEYCLOAK_CLIENT_ID:?NESTJS_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:?KEYCLOAK_CLIENT_SECRET is required} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:?KEYCLOAK_ADMIN is required} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} + KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} + CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} + OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} + OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} + OPENCDS_API_TLS_REJECT_UNAUTHORIZED: ${NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED:-true} develop: watch: - action: sync @@ -179,9 +196,17 @@ services: condition: service_started ports: - 8080:8080 - env_file: - - ../apps/client/.env.docker - - ../apps/client/.env.integ + environment: + SERVER_HOST: ${CLIENT_SERVER_HOST:?CLIENT_SERVER_HOST is required} + SERVER_PORT: ${CLIENT_SERVER_PORT:?CLIENT_SERVER_PORT is required} + CLIENT_PORT: ${CLIENT_PORT:-8080} + KEYCLOAK_PROTOCOL: ${CLIENT_KEYCLOAK_PROTOCOL:?CLIENT_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${CLIENT_KEYCLOAK_DOMAIN:?CLIENT_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${CLIENT_KEYCLOAK_REALM:?CLIENT_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${CLIENT_KEYCLOAK_CLIENT_ID:?CLIENT_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_REDIRECT_URI: ${CLIENT_KEYCLOAK_REDIRECT_URI:?CLIENT_KEYCLOAK_REDIRECT_URI is required} + OPENCDS_ENABLED: ${CLIENT_OPENCDS_ENABLED:-false} + CONTACT_EMAIL: ${CLIENT_CONTACT_EMAIL:?CLIENT_CONTACT_EMAIL is required} develop: watch: - action: sync From 3d9c44914617ddb1a5051f726921511438abf682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 29 Sep 2026 15:39:53 +0200 Subject: [PATCH 08/14] ci: install publishing tools with mise Use the pinned toolchain in package publishing and Sonar jobs. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- .github/workflows/job-npm.yml | 25 ++++++++++--------------- .github/workflows/job-tests-unit.yml | 14 +++++--------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/.github/workflows/job-npm.yml b/.github/workflows/job-npm.yml index 51e4f3eedc..560bf92fb2 100644 --- a/.github/workflows/job-npm.yml +++ b/.github/workflows/job-npm.yml @@ -100,11 +100,12 @@ jobs: - name: Checks-out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false + version: 2026.5.15 + install: true + cache: true - name: Check changes id: check @@ -177,11 +178,12 @@ jobs: with: ref: ${{ github.event.ref }} - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false + version: 2026.5.15 + install: true + cache: true - name: Check for new package version id: check-version @@ -199,13 +201,6 @@ jobs: fi echo "REMOTE=$REMOTE" >> $GITHUB_OUTPUT - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - if: ${{ steps.check-version.outputs.REMOTE == 'false' }} - with: - node-version: "${{ inputs.NODE_VERSION }}" - registry-url: "https://registry.npmjs.org" - - name: Get pnpm store directory if: ${{ steps.check-version.outputs.REMOTE == 'false' }} id: pnpm-store diff --git a/.github/workflows/job-tests-unit.yml b/.github/workflows/job-tests-unit.yml index 1d45a1efdb..5d18d6aa62 100644 --- a/.github/workflows/job-tests-unit.yml +++ b/.github/workflows/job-tests-unit.yml @@ -111,16 +111,12 @@ jobs: name: unit-tests-coverage path: ./coverage - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install - with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - node-version: "${{ inputs.NODE_VERSION }}" + version: 2026.5.15 + install: true + cache: true - name: Get pnpm store directory id: pnpm-store From baa56ec3cd6d86349f6033372d4c3b4ec59ef6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 29 Sep 2026 15:42:08 +0200 Subject: [PATCH 09/14] chore: add mise formatting task Expose repository formatting through the supported task interface. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- mise.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mise.toml b/mise.toml index 89cc2c1528..a91790e9bb 100644 --- a/mise.toml +++ b/mise.toml @@ -71,6 +71,9 @@ run = "mise -E ci run client:build-inner" hide = true run = "APP_VERSION=${APP_VERSION:-dev} pnpm --filter client run build" +[tasks.format] +run = "pnpm format" + [tasks.lint] run = "pnpm lint" From 5c16a58bc41d6f871fe2ee4bf09f2f4c440e25aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 28 Sep 2026 17:12:23 +0200 Subject: [PATCH 10/14] docs: document environment and secrets design Document the agreed transition to mise and fnox. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 From 17b8b2f5b7273fafddaff49d75ec3f2f57b28e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 28 Sep 2026 17:24:57 +0200 Subject: [PATCH 11/14] docs: define environment and secrets management Document mise/fnox and restrict the initializer to the frozen Fastify legacy. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- docs/environment-variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment-variables.md b/docs/environment-variables.md index ec5bf337e9..1263f9ef2b 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -1,6 +1,6 @@ # Registre des variables d’environnement -Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. Les noms du tableau sont les contrats des processus ; `mise` préfixe les valeurs propres à un consommateur (`CLIENT_*` ou `NESTJS_*`) et les tâches ou Compose les traduisent explicitement afin d’éviter les collisions frontend/backend. +Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. | Classe | Variables | Source | Consommateurs | | --- | --- | --- | --- | From f37c75f32a2f6d67736031b7a7fcbae115a1321e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Tue, 29 Sep 2026 15:12:24 +0200 Subject: [PATCH 12/14] feat: prepare scoped Docker and CI configuration Add scoped NestJS public configuration and reject non-legacy tracked env files. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- docs/environment-variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 1263f9ef2b..ec5bf337e9 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -1,6 +1,6 @@ # Registre des variables d’environnement -Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. +Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. Les noms du tableau sont les contrats des processus ; `mise` préfixe les valeurs propres à un consommateur (`CLIENT_*` ou `NESTJS_*`) et les tâches ou Compose les traduisent explicitement afin d’éviter les collisions frontend/backend. | Classe | Variables | Source | Consommateurs | | --- | --- | --- | --- | From 7b7e3c8ef74c357030f461b6710bcc32eac77d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 5 Oct 2026 14:49:59 +0200 Subject: [PATCH 13/14] fix(ci): stabilize environment-backed checks Inject NestJS public configuration in unit tests. Remove the obsolete Playwright dotenv load and retry delayed GHCR manifests. Generated with AI assistance, reviewed by @stephanetrebel Refs #2125 --- .github/workflows/job-build.yml | 6 +- .../src/modules/plugin/plugin.module.spec.ts | 9 ++- ci/scripts/create-image-manifest.sh | 16 ++++++ ci/scripts/create-image-manifest.spec.sh | 51 +++++++++++++++++ ci/scripts/run-unit-tests.sh | 9 +++ ci/scripts/run-unit-tests.spec.sh | 56 +++++++++++++++++++ mise.toml | 2 +- playwright/playwright.config.ts | 3 - 8 files changed, 146 insertions(+), 6 deletions(-) create mode 100755 ci/scripts/create-image-manifest.sh create mode 100755 ci/scripts/create-image-manifest.spec.sh create mode 100755 ci/scripts/run-unit-tests.sh create mode 100755 ci/scripts/run-unit-tests.spec.sh diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 4d2690d4c5..5f5d7c96e0 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -187,6 +187,9 @@ jobs: matrix: images: ${{ fromJSON(needs.matrix.outputs.build-matrix) }} steps: + - name: Checks-out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Download artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -220,7 +223,8 @@ jobs: - name: Create manifest list and push working-directory: /tmp/digests/${{ matrix.images.name }} run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + "$GITHUB_WORKSPACE/ci/scripts/create-image-manifest.sh" \ + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ inputs.REGISTRY }}/${{ inputs.NAMESPACE }}/${{ matrix.images.name }}@sha256:%s ' *) - name: Inspect image diff --git a/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts b/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts index 658e110dae..861adf65b4 100644 --- a/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts +++ b/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts @@ -5,9 +5,16 @@ import { PluginModule } from './plugin.module' import { PluginService } from './plugin.service' describe('pluginModule', () => { - afterEach(() => vi.unstubAllEnvs()) + afterEach(() => { + vi.unstubAllEnvs() + vi.unstubAllGlobals() + }) it('resolves PluginService with all plugins enabled', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({ + issuer: 'https://keycloak.test/realms/dso', + jwks_uri: 'https://keycloak.test/realms/dso/protocol/openid-connect/certs', + })))) vi.stubEnv('USE_ARGOCD', 'true') vi.stubEnv('USE_GITLAB', 'true') vi.stubEnv('USE_HARBOR', 'true') diff --git a/ci/scripts/create-image-manifest.sh b/ci/scripts/create-image-manifest.sh new file mode 100755 index 0000000000..d3a88dbe4f --- /dev/null +++ b/ci/scripts/create-image-manifest.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +retry_delays=(10 60 300) + +for delay in "${retry_delays[@]}"; do + if "$@"; then + exit 0 + fi + + printf 'Image manifest creation failed; retrying in %s seconds.\n' "$delay" >&2 + sleep "$delay" +done + +exec "$@" diff --git a/ci/scripts/create-image-manifest.spec.sh b/ci/scripts/create-image-manifest.spec.sh new file mode 100755 index 0000000000..26ab286fea --- /dev/null +++ b/ci/scripts/create-image-manifest.spec.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$DOCKER_CALLS" +attempts="$(wc -l < "$DOCKER_CALLS")" +if [ "$attempts" -le "$DOCKER_FAILURES" ]; then + exit 1 +fi +EOF + +cat > "$tmp_dir/bin/sleep" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$SLEEP_CALLS" +EOF + +chmod +x "$tmp_dir/bin/docker" "$tmp_dir/bin/sleep" +export PATH="$tmp_dir/bin:$PATH" + +run_manifest() { + local failures="$1" + export DOCKER_FAILURES="$failures" + export DOCKER_CALLS="$tmp_dir/docker-$failures.calls" + export SLEEP_CALLS="$tmp_dir/sleep-$failures.calls" + + "$project_dir/ci/scripts/create-image-manifest.sh" docker buildx imagetools create image:tag image@sha256:digest +} + +run_manifest 3 + +test "$(wc -l < "$tmp_dir/docker-3.calls")" -eq 4 +grep -Fqx '10' "$tmp_dir/sleep-3.calls" +grep -Fqx '60' "$tmp_dir/sleep-3.calls" +grep -Fqx '300' "$tmp_dir/sleep-3.calls" + +if run_manifest 4; then + printf 'Manifest creation unexpectedly succeeded after all retries.\n' >&2 + exit 1 +fi + +test "$(wc -l < "$tmp_dir/docker-4.calls")" -eq 4 +test "$(wc -l < "$tmp_dir/sleep-4.calls")" -eq 3 diff --git a/ci/scripts/run-unit-tests.sh b/ci/scripts/run-unit-tests.sh new file mode 100755 index 0000000000..9d9302e5fd --- /dev/null +++ b/ci/scripts/run-unit-tests.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +for name in "${!NESTJS_@}"; do + export "${name#NESTJS_}=${!name}" +done + +exec fnox exec -- pnpm test:cov diff --git a/ci/scripts/run-unit-tests.spec.sh b/ci/scripts/run-unit-tests.spec.sh new file mode 100755 index 0000000000..d12b817ab7 --- /dev/null +++ b/ci/scripts/run-unit-tests.spec.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/fnox" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[ "$1" = "exec" ] +shift +exec "$@" +EOF + +cat > "$tmp_dir/bin/pnpm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" > "$PNPM_CALL" +env | sort > "$PNPM_ENV" +EOF + +chmod +x "$tmp_dir/bin/fnox" "$tmp_dir/bin/pnpm" +export PATH="$tmp_dir/bin:$PATH" +export PNPM_CALL="$tmp_dir/pnpm.call" +export PNPM_ENV="$tmp_dir/pnpm.env" + +unset KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN KEYCLOAK_REALM KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI CONTACT_EMAIL || true +export NESTJS_SERVER_HOST='0.0.0.0' +export NESTJS_SERVER_PORT='3001' +export NESTJS_KEYCLOAK_PROTOCOL='http' +export NESTJS_KEYCLOAK_DOMAIN='keycloak:8080' +export NESTJS_KEYCLOAK_REALM='dso' +export NESTJS_KEYCLOAK_CLIENT_ID='dso-console-backend' +export NESTJS_KEYCLOAK_REDIRECT_URI='http://localhost:8080' +export NESTJS_CONTACT_EMAIL='cloudpinative-relations@interieur.gouv.fr' +export NESTJS_GRAFANA_URL='https://grafana.example.com' +export NESTJS_DSO_OBSERVABILITY_CHART_VERSION='dso-observability-0.1.7' +export NESTJS_FUTURE_PUBLIC_VALUE='available' + +"$project_dir/ci/scripts/run-unit-tests.sh" + +grep -Fqx 'test:cov' "$PNPM_CALL" +grep -Fqx 'SERVER_HOST=0.0.0.0' "$PNPM_ENV" +grep -Fqx 'SERVER_PORT=3001' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_PROTOCOL=http' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_DOMAIN=keycloak:8080' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_REALM=dso' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_CLIENT_ID=dso-console-backend' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_REDIRECT_URI=http://localhost:8080' "$PNPM_ENV" +grep -Fqx 'CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr' "$PNPM_ENV" +grep -Fqx 'GRAFANA_URL=https://grafana.example.com' "$PNPM_ENV" +grep -Fqx 'DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7' "$PNPM_ENV" +grep -Fqx 'FUTURE_PUBLIC_VALUE=available' "$PNPM_ENV" diff --git a/mise.toml b/mise.toml index a91790e9bb..dcf8a2b147 100644 --- a/mise.toml +++ b/mise.toml @@ -78,7 +78,7 @@ run = "pnpm format" run = "pnpm lint" [tasks."test:unit"] -run = "fnox exec -- pnpm test" +run = "./ci/scripts/run-unit-tests.sh" [tasks."playwright:test"] run = "fnox -P ci exec -- pnpm --dir playwright exec playwright test --grep @e2e" diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index ec4838c6da..c6aa2c62f8 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -1,8 +1,5 @@ -import path from 'node:path' import { defineConfig, devices } from '@playwright/test' -process.loadEnvFile(path.resolve(__dirname, '..', 'apps/client', '.env.docker')) - const isIntegration = process.env.INTEGRATION === 'true' /** From 60ae94e0bcab4152ca7ae4ab80b5d4812244031c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20TR=C3=89BEL=20=28Perso=29?= Date: Mon, 19 Oct 2026 16:29:49 +0200 Subject: [PATCH 14/14] fix(env): address environment migration review feedback Share NestJS environment mapping across launchers, pass explicit plugin flags to Compose, and resolve CI configuration for Playwright consumers. Keep fnox bootstrap writes isolated and send demonstration values via stdin. Remove unused workflow Node inputs and exercise environment regressions in CI. Refs #2125 Generated with AI assistance, reviewed by @stephanetrebel --- .github/workflows/job-lint.yml | 18 ++--- .github/workflows/job-npm.yml | 7 -- .github/workflows/job-playwright.yml | 16 ++-- .github/workflows/job-tests-unit.yml | 9 --- .../workflow-continuous-integration.yml | 6 -- .github/workflows/workflow-handle-hotfix.yml | 2 - .github/workflows/workflow-handle-main.yml | 2 - .github/workflows/workflow-merge-queue.yml | 7 -- .github/workflows/workflow-post-merge.yml | 14 ---- apps/server-nestjs/src/main.module.ts | 2 +- .../sonarqube-client.service.spec.ts | 2 +- .../src/modules/zone/zone.service.spec.ts | 2 +- ci/scripts/check-compose-environment.spec.sh | 31 ++++++++ ci/scripts/check-workflow-environment.spec.sh | 18 +++++ ci/scripts/run-unit-tests.sh | 7 +- docker/docker-compose.ci.yml | 7 ++ docker/docker-compose.dev.yml | 7 ++ docker/docker-compose.integ.yml | 7 ++ mise.toml | 6 +- scripts/bootstrap-secrets.sh | 9 ++- scripts/bootstrap-secrets.spec.sh | 59 +++++++++++++-- scripts/with-nestjs-env.sh | 9 +++ scripts/with-nestjs-env.spec.sh | 73 +++++++++++++++++++ 23 files changed, 233 insertions(+), 87 deletions(-) create mode 100755 ci/scripts/check-compose-environment.spec.sh create mode 100755 ci/scripts/check-workflow-environment.spec.sh create mode 100755 scripts/with-nestjs-env.sh create mode 100755 scripts/with-nestjs-env.spec.sh diff --git a/.github/workflows/job-lint.yml b/.github/workflows/job-lint.yml index 0f8175ce52..8ce5aa96f7 100644 --- a/.github/workflows/job-lint.yml +++ b/.github/workflows/job-lint.yml @@ -5,16 +5,7 @@ permissions: on: workflow_call: - inputs: - NODE_VERSION: - required: false - type: string workflow_dispatch: - inputs: - NODE_VERSION: - description: Nodejs version used to run tests - required: true - type: string jobs: lint: @@ -54,6 +45,15 @@ jobs: - name: Check environment-file boundary run: ./ci/scripts/check-environment-files.sh + - name: Test environment launchers and configuration + run: | + ./scripts/with-nestjs-env.spec.sh + ./scripts/bootstrap-secrets.spec.sh + ./ci/scripts/run-unit-tests.spec.sh + ./ci/scripts/check-workflow-environment.spec.sh + ./ci/scripts/init-env.sh + ./ci/scripts/check-compose-environment.spec.sh + - name: Validate nginx-strangler config run: | sudo apt-get update -qq diff --git a/.github/workflows/job-npm.yml b/.github/workflows/job-npm.yml index 560bf92fb2..ba5a7f9fe6 100644 --- a/.github/workflows/job-npm.yml +++ b/.github/workflows/job-npm.yml @@ -6,9 +6,6 @@ permissions: on: workflow_call: inputs: - NODE_VERSION: - required: true - type: string PUBLISH_APPS: required: true type: boolean @@ -23,10 +20,6 @@ on: type: boolean workflow_dispatch: inputs: - NODE_VERSION: - description: Nodejs version used - required: true - type: string PUBLISH_APPS: description: Publish apps modules required: true diff --git a/.github/workflows/job-playwright.yml b/.github/workflows/job-playwright.yml index 6b882f0ae1..4774d5ff01 100644 --- a/.github/workflows/job-playwright.yml +++ b/.github/workflows/job-playwright.yml @@ -6,18 +6,11 @@ permissions: on: workflow_call: inputs: - NODE_VERSION: - required: true - type: string TAG: required: true type: string workflow_dispatch: inputs: - NODE_VERSION: - description: Nodejs version used to run tests - required: true - type: string TAG: description: Container images version to use for console client and server/server-nestjs required: true @@ -56,7 +49,7 @@ jobs: docker pull "${GHCR_CACHE}/cloud-pi-native/console/${img}:${{ inputs.TAG }}" docker tag "${GHCR_CACHE}/cloud-pi-native/console/${img}:${{ inputs.TAG }}" "dso-console/${img}:ci" done - docker compose -f ./docker/docker-compose.ci.yml up --no-build -d --remove-orphans + mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml up --no-build -d --remove-orphans # See https://playwright.dev/docs/test-sharding for how Playwright handles test sharding - name: Run Playwright tests @@ -65,15 +58,16 @@ jobs: - name: Dump docker compose state on failure if: ${{ failure() }} run: | - docker compose -f ./docker/docker-compose.ci.yml ps -a + mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml ps -a for service in keycloak postgres server server-nestjs client nginx-strangler opencds-mockoon; do echo -e "\nInspecting ${service}..." - docker compose -f ./docker/docker-compose.ci.yml logs --no-color ${service} + mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml logs --no-color ${service} docker inspect dso-console_${service} --format '{{json .State}}' done - name: Clean up docker resources (containers, volumes) - run: docker compose -f ./docker/docker-compose.ci.yml down -v --remove-orphans + if: ${{ always() }} + run: mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml down -v --remove-orphans - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ !cancelled() }} diff --git a/.github/workflows/job-tests-unit.yml b/.github/workflows/job-tests-unit.yml index 5d18d6aa62..aed7911519 100644 --- a/.github/workflows/job-tests-unit.yml +++ b/.github/workflows/job-tests-unit.yml @@ -5,10 +5,6 @@ permissions: on: workflow_call: - inputs: - NODE_VERSION: - required: true - type: string secrets: SONAR_HOST_URL: required: false @@ -17,11 +13,6 @@ on: SONAR_PROJECT_KEY: required: false workflow_dispatch: - inputs: - NODE_VERSION: - description: Nodejs version used to run tests - required: true - type: string jobs: unit-tests: diff --git a/.github/workflows/workflow-continuous-integration.yml b/.github/workflows/workflow-continuous-integration.yml index 09b5b24d26..9508609375 100644 --- a/.github/workflows/workflow-continuous-integration.yml +++ b/.github/workflows/workflow-continuous-integration.yml @@ -19,7 +19,6 @@ on: workflow_dispatch: env: - NODE_VERSION: 26.7.0 REGISTRY: ghcr.io NAMESPACE: "${{ github.repository }}" BUILD_AMD64: true @@ -56,7 +55,6 @@ jobs: expose-vars: runs-on: ubuntu-latest outputs: - NODE_VERSION: ${{ env.NODE_VERSION }} REGISTRY: ${{ env.REGISTRY }} NAMESPACE: ${{ env.NAMESPACE }} BUILD_AMD64: ${{ env.BUILD_AMD64 }} @@ -70,8 +68,6 @@ jobs: uses: ./.github/workflows/job-lint.yml needs: - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} unit-tests: uses: ./.github/workflows/job-tests-unit.yml @@ -79,8 +75,6 @@ jobs: needs: - path-filter - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} secrets: SONAR_HOST_URL: "${{ secrets.SONAR_HOST_URL }}" SONAR_TOKEN: "${{ secrets.SONAR_TOKEN }}" diff --git a/.github/workflows/workflow-handle-hotfix.yml b/.github/workflows/workflow-handle-hotfix.yml index bebe71ef42..493b60986e 100644 --- a/.github/workflows/workflow-handle-hotfix.yml +++ b/.github/workflows/workflow-handle-hotfix.yml @@ -19,7 +19,6 @@ env: BUILD_AMD64: true BUILD_ARM64: true USE_QEMU: false - NODE_VERSION: 26.7.0 jobs: expose-vars: @@ -30,7 +29,6 @@ jobs: BUILD_AMD64: ${{ env.BUILD_AMD64 }} BUILD_ARM64: ${{ env.BUILD_ARM64 }} USE_QEMU: ${{ env.USE_QEMU }} - NODE_VERSION: ${{ env.NODE_VERSION }} steps: - name: Exposing env vars run: echo "Exposing env vars" diff --git a/.github/workflows/workflow-handle-main.yml b/.github/workflows/workflow-handle-main.yml index 7b73196e86..57210adaee 100644 --- a/.github/workflows/workflow-handle-main.yml +++ b/.github/workflows/workflow-handle-main.yml @@ -19,7 +19,6 @@ env: BUILD_AMD64: true BUILD_ARM64: true USE_QEMU: false - NODE_VERSION: 26.7.0 jobs: expose-vars: @@ -30,7 +29,6 @@ jobs: BUILD_AMD64: ${{ env.BUILD_AMD64 }} BUILD_ARM64: ${{ env.BUILD_ARM64 }} USE_QEMU: ${{ env.USE_QEMU }} - NODE_VERSION: ${{ env.NODE_VERSION }} steps: - name: Exposing env vars run: echo "Exposing env vars" diff --git a/.github/workflows/workflow-merge-queue.yml b/.github/workflows/workflow-merge-queue.yml index cc7673b447..25e73a5d39 100644 --- a/.github/workflows/workflow-merge-queue.yml +++ b/.github/workflows/workflow-merge-queue.yml @@ -18,7 +18,6 @@ on: types: [checks_requested] env: - NODE_VERSION: 26.7.0 REGISTRY: ghcr.io NAMESPACE: "${{ github.repository }}" BUILD_AMD64: true @@ -60,7 +59,6 @@ jobs: expose-vars: runs-on: ubuntu-latest outputs: - NODE_VERSION: ${{ env.NODE_VERSION }} REGISTRY: ${{ env.REGISTRY }} NAMESPACE: ${{ env.NAMESPACE }} BUILD_AMD64: ${{ env.BUILD_AMD64 }} @@ -76,8 +74,6 @@ jobs: uses: ./.github/workflows/job-lint.yml needs: - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} unit-tests: uses: ./.github/workflows/job-tests-unit.yml @@ -85,8 +81,6 @@ jobs: needs: - path-filter - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} secrets: SONAR_HOST_URL: "${{ secrets.SONAR_HOST_URL }}" SONAR_TOKEN: "${{ secrets.SONAR_TOKEN }}" @@ -125,7 +119,6 @@ jobs: - expose-vars - build with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} TAG: pr-${{ github.event.inputs.PR_NUMBER || github.event.number || needs.expose-vars.outputs.MERGE_GROUP_HEAD_SHA }} scan-vuln: diff --git a/.github/workflows/workflow-post-merge.yml b/.github/workflows/workflow-post-merge.yml index 3e6190088c..3d54964d4a 100644 --- a/.github/workflows/workflow-post-merge.yml +++ b/.github/workflows/workflow-post-merge.yml @@ -5,9 +5,6 @@ on: branches: - main -env: - NODE_VERSION: 26.7.0 - permissions: contents: read id-token: write @@ -33,22 +30,11 @@ jobs: - 'packages/**' - 'plugins/**' - expose-vars: - runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} - outputs: - NODE_VERSION: ${{ env.NODE_VERSION }} - steps: - - name: Exposing env vars - run: echo "Exposing env vars." - npm-publish: uses: ./.github/workflows/job-npm.yml needs: - path-filter - - expose-vars with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} PUBLISH_PACKAGES: true PUBLISH_PLUGINS: true CHECK_CHANGES: true diff --git a/apps/server-nestjs/src/main.module.ts b/apps/server-nestjs/src/main.module.ts index 92b26c8d28..13b62d059a 100644 --- a/apps/server-nestjs/src/main.module.ts +++ b/apps/server-nestjs/src/main.module.ts @@ -20,8 +20,8 @@ import { ProjectSecretsModule } from './modules/project-secrets/project-secrets. import { ProjectServicesModule } from './modules/project-services/project-services.module' import { ProjectModule } from './modules/project/project.module' import { RepositoryModule } from './modules/repository/repository.module' -import { SystemConfigModule } from './modules/system-config/system-config.module' import { StageModule } from './modules/stage/stage.module' +import { SystemConfigModule } from './modules/system-config/system-config.module' import { SystemSettingsModule } from './modules/system-settings/system-settings.module' import { UserModule } from './modules/user/user.module' import { VersionModule } from './modules/version/version.module' diff --git a/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts b/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts index dc9680e339..e80938895d 100644 --- a/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts +++ b/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts @@ -1,11 +1,11 @@ import type { ConfigType } from '@nestjs/config' +import type { DeepMockProxy } from 'vitest-mock-extended' import type { AddPermissionGroupParams, CreatePermissionTemplateParams, CreateProjectParams, CreateUserGroupParams, CreateUserParams, DeactivateUserParams, RevokeUserTokenParams } from './sonarqube-client.service' import { faker } from '@faker-js/faker' import { Test } from '@nestjs/testing' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' -import type { DeepMockProxy } from 'vitest-mock-extended' import { mockDeep } from 'vitest-mock-extended' import { sonarqubeConfigFactory } from '../../config/sonarqube.config' import { getAll } from '../../utils/iterable.utils' diff --git a/apps/server-nestjs/src/modules/zone/zone.service.spec.ts b/apps/server-nestjs/src/modules/zone/zone.service.spec.ts index 427af6e1de..8b67aaf2e1 100644 --- a/apps/server-nestjs/src/modules/zone/zone.service.spec.ts +++ b/apps/server-nestjs/src/modules/zone/zone.service.spec.ts @@ -1,12 +1,12 @@ import type { Cluster, Prisma } from '@prisma/client' import type { DeepMockProxy } from 'vitest-mock-extended' +import type { Zone } from './zone-queries.utils' import { faker } from '@faker-js/faker' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mock, mockDeep } from 'vitest-mock-extended' import { AppEventsService } from '../events/app-events.service' import { PrismaService } from '../infrastructure/database/prisma.service' -import type { Zone } from './zone-queries.utils' import { makeZone } from './zone-testing.utils' import { ZoneService } from './zone.service' diff --git a/ci/scripts/check-compose-environment.spec.sh b/ci/scripts/check-compose-environment.spec.sh new file mode 100755 index 0000000000..268472b23f --- /dev/null +++ b/ci/scripts/check-compose-environment.spec.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# Use demonstration CI secrets even for dev/integ to avoid reading local pass entries. +for profile in docker ci integ; do + compose_profile="$profile" + [ "$profile" != docker ] || compose_profile=dev + KUBECONFIG_HOST_PATH=/tmp/console-test-kubeconfig EXTERNAL_PLUGINS_DIR_HOST_PATH=/tmp/console-test-plugins \ + mise -E "$profile" exec -- fnox -P ci exec -- \ + docker compose -f "docker/docker-compose.$compose_profile.yml" config --format json | + node --input-type=module -e ' + import assert from "node:assert/strict" + let input = "" + for await (const chunk of process.stdin) input += chunk + const environment = JSON.parse(input).services["server-nestjs"].environment + for (const plugin of ["ARGOCD", "GITLAB", "HARBOR", "NEXUS", "OBSERVABILITY", "SONARQUBE", "VAULT"]) { + assert.equal(environment["USE_" + plugin], "false", "USE_" + plugin + " must be explicitly disabled") + } + assert.equal(environment.USE_SERVICE_CHAIN, process.argv[1] === "integ" ? "false" : "true") + if (process.argv[1] === "ci") { + assert.equal(environment.NODE_ENV, "production") + assert.equal(environment.KEYCLOAK_DOMAIN, "keycloak:8080") + assert.equal(new URL(environment.DB_URL).hostname, "postgres") + assert.equal(JSON.parse(input).services.client.environment.SERVER_HOST, "nginx-strangler") + } + ' "$profile" +done +printf 'Compose plugin environment regression tests passed.\n' diff --git a/ci/scripts/check-workflow-environment.spec.sh b/ci/scripts/check-workflow-environment.spec.sh new file mode 100755 index 0000000000..de20bba05d --- /dev/null +++ b/ci/scripts/check-workflow-environment.spec.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +workflow=.github/workflows/job-playwright.yml +if grep -n 'docker compose' "$workflow" | grep -v 'name:' | grep -vF 'mise -E ci exec -- fnox exec -- docker compose'; then + printf 'Every Playwright Compose command must resolve CI configuration and secrets.\n' >&2 + exit 1 +fi +test "$(grep -cF 'mise -E ci exec -- fnox exec -- docker compose' "$workflow")" -eq 4 +grep -A 2 'name: Clean up docker resources' "$workflow" | grep -Fq "if: \${{ always() }}" +if grep -Rn 'NODE_VERSION' .github/workflows; then + printf 'Node must be pinned only by mise, not by unused workflow inputs.\n' >&2 + exit 1 +fi +printf 'Workflow environment regression tests passed.\n' diff --git a/ci/scripts/run-unit-tests.sh b/ci/scripts/run-unit-tests.sh index 9d9302e5fd..7da22a5888 100755 --- a/ci/scripts/run-unit-tests.sh +++ b/ci/scripts/run-unit-tests.sh @@ -2,8 +2,5 @@ set -euo pipefail -for name in "${!NESTJS_@}"; do - export "${name#NESTJS_}=${!name}" -done - -exec fnox exec -- pnpm test:cov +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +exec "$project_dir/scripts/with-nestjs-env.sh" pnpm test:cov diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index 04a7e8ee59..48a44535fd 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -103,7 +103,14 @@ services: KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_ARGOCD: ${NESTJS_USE_ARGOCD:-false} + USE_GITLAB: ${NESTJS_USE_GITLAB:-false} + USE_HARBOR: ${NESTJS_USE_HARBOR:-false} + USE_NEXUS: ${NESTJS_USE_NEXUS:-false} + USE_OBSERVABILITY: ${NESTJS_USE_OBSERVABILITY:-false} USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + USE_SONARQUBE: ${NESTJS_USE_SONARQUBE:-false} + USE_VAULT: ${NESTJS_USE_VAULT:-false} OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index e1e9ea8a90..bf8970c30c 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -149,7 +149,14 @@ services: KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_ARGOCD: ${NESTJS_USE_ARGOCD:-false} + USE_GITLAB: ${NESTJS_USE_GITLAB:-false} + USE_HARBOR: ${NESTJS_USE_HARBOR:-false} + USE_NEXUS: ${NESTJS_USE_NEXUS:-false} + USE_OBSERVABILITY: ${NESTJS_USE_OBSERVABILITY:-false} USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + USE_SONARQUBE: ${NESTJS_USE_SONARQUBE:-false} + USE_VAULT: ${NESTJS_USE_VAULT:-false} OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} diff --git a/docker/docker-compose.integ.yml b/docker/docker-compose.integ.yml index 123d6860c4..be2ed30106 100644 --- a/docker/docker-compose.integ.yml +++ b/docker/docker-compose.integ.yml @@ -125,7 +125,14 @@ services: KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_ARGOCD: ${NESTJS_USE_ARGOCD:-false} + USE_GITLAB: ${NESTJS_USE_GITLAB:-false} + USE_HARBOR: ${NESTJS_USE_HARBOR:-false} + USE_NEXUS: ${NESTJS_USE_NEXUS:-false} + USE_OBSERVABILITY: ${NESTJS_USE_OBSERVABILITY:-false} USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + USE_SONARQUBE: ${NESTJS_USE_SONARQUBE:-false} + USE_VAULT: ${NESTJS_USE_VAULT:-false} OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} diff --git a/mise.toml b/mise.toml index dcf8a2b147..e3718c8d56 100644 --- a/mise.toml +++ b/mise.toml @@ -52,14 +52,14 @@ run = "./ci/scripts/init-env.sh && ./scripts/bootstrap-secrets.sh" run = "docker compose -f docker/docker-compose.local.yml up -d --remove-orphans" [tasks."server-nestjs:dev"] -run = "SERVER_HOST=$NESTJS_SERVER_HOST SERVER_PORT=$NESTJS_SERVER_PORT KEYCLOAK_PROTOCOL=$NESTJS_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$NESTJS_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$NESTJS_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$NESTJS_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$NESTJS_KEYCLOAK_REDIRECT_URI CONTACT_EMAIL=$NESTJS_CONTACT_EMAIL fnox exec -- pnpm --filter server-nestjs run start:dev" +run = "./scripts/with-nestjs-env.sh pnpm --filter server-nestjs run start:dev" [tasks."server-nestjs:test"] run = "mise -E ci run server-nestjs:test-inner" [tasks."server-nestjs:test-inner"] hide = true -run = "NODE_ENV=test SERVER_HOST=$NESTJS_SERVER_HOST SERVER_PORT=$NESTJS_SERVER_PORT KEYCLOAK_PROTOCOL=$NESTJS_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$NESTJS_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$NESTJS_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$NESTJS_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$NESTJS_KEYCLOAK_REDIRECT_URI CONTACT_EMAIL=$NESTJS_CONTACT_EMAIL fnox exec -- pnpm --filter server-nestjs test" +run = "NODE_ENV=test ./scripts/with-nestjs-env.sh pnpm --filter server-nestjs test" [tasks."client:dev"] run = "SERVER_HOST=$CLIENT_SERVER_HOST SERVER_PORT=$CLIENT_SERVER_PORT CLIENT_PORT=$CLIENT_PORT KEYCLOAK_PROTOCOL=$CLIENT_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$CLIENT_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$CLIENT_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$CLIENT_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$CLIENT_KEYCLOAK_REDIRECT_URI OPENCDS_ENABLED=$CLIENT_OPENCDS_ENABLED CONTACT_EMAIL=$CLIENT_CONTACT_EMAIL pnpm --filter client run dev" @@ -81,7 +81,7 @@ run = "pnpm lint" run = "./ci/scripts/run-unit-tests.sh" [tasks."playwright:test"] -run = "fnox -P ci exec -- pnpm --dir playwright exec playwright test --grep @e2e" +run = "KEYCLOAK_PROTOCOL=$CLIENT_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$CLIENT_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$CLIENT_KEYCLOAK_REALM KEYCLOAK_REDIRECT_URI=$CLIENT_KEYCLOAK_REDIRECT_URI fnox -P ci exec -- pnpm --dir playwright exec playwright test --grep @e2e" [tasks."docker:dev"] run = "mise -E docker run docker:dev-inner" diff --git a/scripts/bootstrap-secrets.sh b/scripts/bootstrap-secrets.sh index 29cf8133ab..4b741c8e88 100755 --- a/scripts/bootstrap-secrets.sh +++ b/scripts/bootstrap-secrets.sh @@ -19,9 +19,16 @@ if [ "$confirmation" != "y" ] && [ "$confirmation" != "Y" ]; then exit 0 fi +# fnox set rewrites its config even when the secret reference already exists. +# Write through a disposable copy so contributor edits are never overwritten. +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +cp "$project_dir/fnox.toml" "$tmp_dir/fnox.toml" + while IFS='|' read -r name value; do [ -n "$name" ] || continue - fnox get "$name" >/dev/null 2>&1 || fnox set "$name" "$value" --provider pass + fnox get "$name" >/dev/null 2>&1 || printf '%s' "$value" | fnox --config "$tmp_dir/fnox.toml" set "$name" --provider pass done <<'SECRETS' SESSION_SECRET|a-very-strong-secret-with-more-than-32-char DB_URL|postgresql://admin:admin@localhost:5432/dso-console-db?schema=public diff --git a/scripts/bootstrap-secrets.spec.sh b/scripts/bootstrap-secrets.spec.sh index b88811eb6f..334ad0878c 100755 --- a/scripts/bootstrap-secrets.spec.sh +++ b/scripts/bootstrap-secrets.spec.sh @@ -5,7 +5,11 @@ set -euo pipefail project_dir="$(git rev-parse --show-toplevel)" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT -mkdir -p "$tmp_dir/bin" +mkdir -p "$tmp_dir/bin" "$tmp_dir/project/scripts" "$tmp_dir/temporary-configs" +cp "$project_dir/scripts/bootstrap-secrets.sh" "$tmp_dir/project/scripts/" +cp "$project_dir/fnox.toml" "$tmp_dir/project/fnox.toml" +printf '\n# Preserve contributor edits too.\n' >> "$tmp_dir/project/fnox.toml" +cp "$tmp_dir/project/fnox.toml" "$tmp_dir/expected.toml" cat > "$tmp_dir/bin/pass" <<'EOF' #!/usr/bin/env bash @@ -14,17 +18,56 @@ EOF cat > "$tmp_dir/bin/fnox" <<'EOF' #!/usr/bin/env bash -if [ "$1" = "get" ]; then - exit 1 +set -euo pipefail +config=fnox.toml +if [ "$1" = --config ]; then + config="$2" + shift 2 +fi +if [ "$1" = get ]; then + [ "${EXISTING_SECRETS:-false}" = true ] + exit fi printf '%s\n' "$*" >> "$FNOX_CALLS" +[ "$1" = set ] +name="$2" +shift 2 +# Simulate fnox rewriting the selected config, including when storage fails. +printf '\n# fnox rewrote config\n' >> "$config" +[ "$1" = --provider ] || { printf 'Secrets must not appear in argv\n' >&2; exit 1; } +[ "$2" = pass ] +value="$(> "$FNOX_VALUES" +exit "${FNOX_EXIT:-0}" EOF -chmod +x "$tmp_dir/bin/pass" "$tmp_dir/bin/fnox" +chmod +x "$tmp_dir/bin/"* export FNOX_CALLS="$tmp_dir/fnox.calls" +export FNOX_VALUES="$tmp_dir/fnox.values" +export TMPDIR="$tmp_dir/temporary-configs" export PATH="$tmp_dir/bin:$PATH" -printf 'y\n' | "$project_dir/scripts/bootstrap-secrets.sh" +cd "$tmp_dir/project" + +printf 'y\n' | ./scripts/bootstrap-secrets.sh +cmp fnox.toml "$tmp_dir/expected.toml" +test "$(grep -c '^set ' "$FNOX_CALLS")" -eq 6 +grep -Fqx 'set SESSION_SECRET --provider pass' "$FNOX_CALLS" +grep -Fqx 'SESSION_SECRET|a-very-strong-secret-with-more-than-32-char' "$FNOX_VALUES" +grep -Fqx 'OPENCDS_API_TOKEN|token' "$FNOX_VALUES" +test -z "$(ls -A "$TMPDIR")" -test "$(grep -c '^set ' "$tmp_dir/fnox.calls")" -eq 6 -grep -Fqx 'set SESSION_SECRET a-very-strong-secret-with-more-than-32-char --provider pass' "$tmp_dir/fnox.calls" -grep -Fqx 'set OPENCDS_API_TOKEN token --provider pass' "$tmp_dir/fnox.calls" +rm "$FNOX_CALLS" +printf 'y\n' | EXISTING_SECRETS=true ./scripts/bootstrap-secrets.sh +test ! -e "$FNOX_CALLS" +printf 'n\n' | ./scripts/bootstrap-secrets.sh +test ! -e "$FNOX_CALLS" + +if printf 'y\n' | FNOX_EXIT=42 ./scripts/bootstrap-secrets.sh; then + printf 'Bootstrap swallowed a provider failure\n' >&2 + exit 1 +else + test "$?" -eq 42 +fi +cmp fnox.toml "$tmp_dir/expected.toml" +test -z "$(ls -A "$TMPDIR")" +printf 'Secret bootstrap regression tests passed.\n' diff --git a/scripts/with-nestjs-env.sh b/scripts/with-nestjs-env.sh new file mode 100755 index 0000000000..d4d70acdb4 --- /dev/null +++ b/scripts/with-nestjs-env.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +for name in "${!NESTJS_@}"; do + export "${name#NESTJS_}=${!name}" +done + +exec fnox exec -- "$@" diff --git a/scripts/with-nestjs-env.spec.sh b/scripts/with-nestjs-env.spec.sh new file mode 100755 index 0000000000..ed4ec082ba --- /dev/null +++ b/scripts/with-nestjs-env.spec.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +cd "$project_dir" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/fnox" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[ "$1" != -P ] || shift 2 +[ "$1" = exec ] +shift +[ "$1" = -- ] && shift +exec "$@" +EOF + +cat > "$tmp_dir/bin/pnpm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" > "$PNPM_CALL" +env > "$PNPM_ENV" +exit "${PNPM_EXIT:-0}" +EOF +chmod +x "$tmp_dir/bin/"* + +for task in server-nestjs:dev server-nestjs:test-inner test:unit playwright:test; do + command="$(mise tasks info "$task" --json | node --input-type=module -e ' + let input = "" + for await (const chunk of process.stdin) input += chunk + process.stdout.write(JSON.parse(input).run.join("\n")) + ')" + env -i PATH="$tmp_dir/bin:$PATH" HOME="$HOME" PNPM_CALL="$tmp_dir/call" PNPM_ENV="$tmp_dir/env" \ + NESTJS_SERVER_HOST=localhost NESTJS_SERVER_PORT=3001 \ + NESTJS_KEYCLOAK_PROTOCOL=http NESTJS_KEYCLOAK_DOMAIN=localhost:8090 \ + NESTJS_KEYCLOAK_REALM=dso NESTJS_KEYCLOAK_CLIENT_ID=backend \ + NESTJS_KEYCLOAK_REDIRECT_URI=http://localhost:8080 NESTJS_CONTACT_EMAIL=demo@example.com \ + NESTJS_USE_VAULT=false NESTJS_USE_GITLAB=false NESTJS_PROJECTS_ROOT_DIR='' \ + NESTJS_FUTURE_PUBLIC_VALUE='value with spaces' \ + CLIENT_KEYCLOAK_PROTOCOL=https CLIENT_KEYCLOAK_DOMAIN=browser.example.com \ + CLIENT_KEYCLOAK_REALM=browser-realm CLIENT_KEYCLOAK_REDIRECT_URI=https://console.example.com \ + bash -c "$command" + if [ "$task" = playwright:test ]; then + grep -Fqx 'KEYCLOAK_PROTOCOL=https' "$tmp_dir/env" + grep -Fqx 'KEYCLOAK_DOMAIN=browser.example.com' "$tmp_dir/env" + grep -Fqx 'KEYCLOAK_REALM=browser-realm' "$tmp_dir/env" + grep -Fqx 'KEYCLOAK_REDIRECT_URI=https://console.example.com' "$tmp_dir/env" + else + grep -Fqx 'USE_VAULT=false' "$tmp_dir/env" + grep -Fqx 'USE_GITLAB=false' "$tmp_dir/env" + grep -Fqx 'PROJECTS_ROOT_DIR=' "$tmp_dir/env" + grep -Fqx 'FUTURE_PUBLIC_VALUE=value with spaces' "$tmp_dir/env" + fi + case "$task" in + server-nestjs:dev) grep -Fqx -- '--filter server-nestjs run start:dev' "$tmp_dir/call" ;; + server-nestjs:test-inner) + grep -Fqx -- '--filter server-nestjs test' "$tmp_dir/call" + grep -Fqx 'NODE_ENV=test' "$tmp_dir/env" + ;; + test:unit) grep -Fqx 'test:cov' "$tmp_dir/call" ;; + playwright:test) grep -Fqx -- '--dir playwright exec playwright test --grep @e2e' "$tmp_dir/call" ;; + esac + if env PATH="$tmp_dir/bin:$PATH" PNPM_CALL="$tmp_dir/call" PNPM_ENV="$tmp_dir/env" PNPM_EXIT=42 bash -c "$command"; then + printf 'Task %s swallowed the child failure\n' "$task" >&2 + exit 1 + else + test "$?" -eq 42 + fi +done +printf 'Application task environment regression tests passed.\n'