Merge pull request #36 from R0m1k3/claude/combohr-app-recreation-plan-yu7wre
Réparer l'intégration continue, et poser des ports internes non communs
This commit is contained in:
6 files changed
+70
-19
No files matched your search
+6
-2
@@ -71,7 +71,11 @@ RUN chmod +x ./docker/entrypoint.sh
|
||||
RUN mkdir -p /data/documents /secrets && chown -R nextjs:nodejs /data /secrets
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000 HOSTNAME=0.0.0.0
|
||||
# Port peu courant jusque **dans** l'image : le reverse-proxy porte alors la
|
||||
# même valeur partout, et rien ne rappelle un 3000 par défaut. Surchargeable
|
||||
# par la pile, qui aligne publication, sonde de santé et serveur sur une
|
||||
# unique variable.
|
||||
EXPOSE 9317
|
||||
ENV PORT=9317 HOSTNAME=0.0.0.0
|
||||
|
||||
CMD ["./docker/entrypoint.sh"]
|
||||
+28
-12
@@ -12,6 +12,11 @@ services:
|
||||
# du même réseau. Le changer reste recommandé.
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-planflow-interne}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-planflow}
|
||||
# Port d'écoute **dans** le conteneur. Un port non commun ne protège de
|
||||
# rien — sur un réseau Docker, chaque conteneur a son adresse et deux
|
||||
# services peuvent écouter le même port sans se gêner — mais il lève
|
||||
# l'ambiguïté quand plusieurs bases cohabitent derrière le même proxy.
|
||||
PGPORT: ${POSTGRES_PORT_INTERNAL:-5439}
|
||||
# Transmis au script d'initialisation, qui crée le rôle de connexion de
|
||||
# l'application — celui-ci ne doit surtout pas être le superutilisateur.
|
||||
APP_DB_USER: ${APP_DB_USER:-planflow_app}
|
||||
@@ -53,13 +58,17 @@ services:
|
||||
sleep 1
|
||||
done
|
||||
) &
|
||||
exec docker-entrypoint.sh postgres
|
||||
exec docker-entrypoint.sh postgres -p "$$PGPORT"
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
# Crée le rôle applicatif à la première initialisation du volume.
|
||||
- ./docker/init-app-role.sh:/docker-entrypoint-initdb.d/10-init-app-role.sh:ro
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-planflow} -d ${POSTGRES_DB:-planflow}']
|
||||
test:
|
||||
[
|
||||
'CMD-SHELL',
|
||||
'pg_isready -p ${POSTGRES_PORT_INTERNAL:-5439} -U ${POSTGRES_USER:-planflow} -d ${POSTGRES_DB:-planflow}',
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -78,10 +87,9 @@ services:
|
||||
networks:
|
||||
- interne
|
||||
- nginx_default
|
||||
# Le conteneur reste sur 5432 pour l'application, et le port hôte est publié
|
||||
# sur un port non commun, configurable : plusieurs instances PostgreSQL
|
||||
# coexistent sur le même serveur, et s'en tenir au 5432 par défaut les
|
||||
# ferait entrer en collision.
|
||||
# Ni le port du conteneur ni celui de l'hôte ne sont les ports usuels :
|
||||
# plusieurs instances PostgreSQL coexistent sur un même serveur, et s'en
|
||||
# tenir au 5432 partout invite la confusion.
|
||||
#
|
||||
# **Lié à la boucle locale**, et c'est important : la publication sert à se
|
||||
# connecter depuis le serveur (sauvegarde, psql), pas depuis le réseau. Sans
|
||||
@@ -89,7 +97,7 @@ services:
|
||||
# données RH derrière un mot de passe par défaut, joignable depuis
|
||||
# l'extérieur. Pour un accès distant, passer par un tunnel.
|
||||
ports:
|
||||
- '${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-55432}:5432'
|
||||
- '${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-55432}:${POSTGRES_PORT_INTERNAL:-5439}'
|
||||
|
||||
# Pose le rôle applicatif à **chaque** démarrage de la pile.
|
||||
#
|
||||
@@ -110,6 +118,7 @@ services:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGHOST: db
|
||||
PGPORT: ${POSTGRES_PORT_INTERNAL:-5439}
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD:-planflow-interne}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-planflow}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-planflow}
|
||||
@@ -134,10 +143,14 @@ services:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
# Port d'écoute du serveur **dans** le conteneur. C'est celui que vise
|
||||
# le reverse-proxy ; il doit rester le même que celui du healthcheck et
|
||||
# de la publication, d'où l'unique variable.
|
||||
PORT: ${APP_PORT_INTERNAL:-9317}
|
||||
# Le compte applicatif, **pas** le superutilisateur d'amorçage : un
|
||||
# superutilisateur contourne la row-level security, y compris déclarée en
|
||||
# FORCE, et l'isolation ne reposerait plus que sur la couche applicative.
|
||||
DATABASE_URL: postgresql://${APP_DB_USER:-planflow_app}:${APP_DB_PASSWORD:-planflow-app-interne}@db:5432/${POSTGRES_DB:-planflow}
|
||||
DATABASE_URL: postgresql://${APP_DB_USER:-planflow_app}:${APP_DB_PASSWORD:-planflow-app-interne}@db:${POSTGRES_PORT_INTERNAL:-5439}/${POSTGRES_DB:-planflow}
|
||||
# Laissée vide, elle est **produite au premier démarrage** et conservée
|
||||
# dans le volume `secrets`. La renseigner ici reste possible pour un
|
||||
# déploiement qui gère ses secrets par ailleurs — mais une variable
|
||||
@@ -158,6 +171,7 @@ services:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-planflow-interne}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-planflow}
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_PORT_INTERNAL: ${POSTGRES_PORT_INTERNAL:-5439}
|
||||
APP_DB_USER: ${APP_DB_USER:-planflow_app}
|
||||
APP_DB_PASSWORD: ${APP_DB_PASSWORD:-planflow-app-interne}
|
||||
volumes:
|
||||
@@ -167,21 +181,23 @@ services:
|
||||
- secrets:/secrets
|
||||
networks:
|
||||
# `interne` pour joindre la base, `nginx_default` pour être joignable par
|
||||
# le reverse-proxy — qui atteint le conteneur sur son port 3000, sans
|
||||
# le reverse-proxy — qui atteint le conteneur sur son port interne, sans
|
||||
# passer par le port publié.
|
||||
- interne
|
||||
- nginx_default
|
||||
ports:
|
||||
# Port peu courant : le service est censé passer par le reverse-proxy, et
|
||||
# un 3000 publié sur l'hôte se heurte à tout ce qui traîne.
|
||||
- '${APP_PORT:-9317}:3000'
|
||||
# un 3000 publié sur l'hôte se heurte à tout ce qui traîne. Le port
|
||||
# **interne** l'est aussi, pour que la configuration du reverse-proxy
|
||||
# porte partout la même valeur.
|
||||
- '${APP_PORT:-9317}:${APP_PORT_INTERNAL:-9317}'
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
'CMD',
|
||||
'node',
|
||||
'-e',
|
||||
"fetch('http://127.0.0.1:3000/api/sante').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||
"fetch('http://127.0.0.1:${APP_PORT_INTERNAL:-9317}/api/sante').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
|
||||
@@ -22,7 +22,10 @@ const superUser = process.env.POSTGRES_USER ?? 'planflow';
|
||||
const superPassword = process.env.POSTGRES_PASSWORD ?? '';
|
||||
const database = process.env.POSTGRES_DB ?? 'planflow';
|
||||
const host = process.env.POSTGRES_HOST ?? 'db';
|
||||
const port = Number(process.env.POSTGRES_PORT_INTERNAL ?? 5432);
|
||||
// Le port d'écoute **dans** le conteneur de la base, que la pile choisit peu
|
||||
// commun. Le défaut suit celui du compose, pas celui de PostgreSQL : une
|
||||
// valeur qui diverge se solderait par un refus de connexion à l'amorçage.
|
||||
const port = Number(process.env.POSTGRES_PORT_INTERNAL ?? 5439);
|
||||
|
||||
const appRole = process.env.APP_DB_USER ?? 'planflow_app';
|
||||
const appPassword = process.env.APP_DB_PASSWORD ?? 'planflow-app-interne';
|
||||
|
||||
@@ -28,7 +28,9 @@ DB_USER="${POSTGRES_USER:-planflow}"
|
||||
# Dans le conteneur `db`, l'hôte est local ; depuis `db-init`, c'est `db`.
|
||||
# L'un et l'autre doivent aboutir au même SQL.
|
||||
if [ -n "${PGHOST:-}" ]; then
|
||||
set -- -h "$PGHOST" -p "${PGPORT:-5432}" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1
|
||||
# Défaut aligné sur celui de la pile, et non sur celui de PostgreSQL : la
|
||||
# base écoute un port peu commun, et 5432 ne joindrait rien.
|
||||
set -- -h "$PGHOST" -p "${PGPORT:-5439}" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1
|
||||
else
|
||||
set -- -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1
|
||||
fi
|
||||
|
||||
+12
-1
@@ -819,7 +819,18 @@ async function seedPlanning(accountId: string): Promise<void> {
|
||||
isoWeek: week.isoWeek,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
// L'état de publication est **réimposé**, pas seulement posé à la
|
||||
// création. Un seed qui laisse en place ce qu'il trouve ne produit un
|
||||
// état de départ connu que la première fois : la semaine précédente,
|
||||
// publiée par définition, reste en brouillon dès lors qu'elle a déjà
|
||||
// été semée quand elle était la semaine courante. Les tests qui
|
||||
// s'appuient dessus échouent alors sur une base de développement et
|
||||
// passent en intégration continue, où le semis est neuf — l'écart le
|
||||
// plus coûteux à diagnostiquer.
|
||||
update: {
|
||||
status: week === previous ? 'PUBLISHED' : 'DRAFT',
|
||||
publishedAt: week === previous ? new Date() : null,
|
||||
},
|
||||
create: {
|
||||
accountId,
|
||||
teamId: team.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { formatMonthParam, monthOf, previousMonth } from '../../src/domain/planning/month';
|
||||
import { formatMonthParam, monthOf } from '../../src/domain/planning/month';
|
||||
|
||||
/**
|
||||
* Heures et périodes de paie.
|
||||
@@ -10,7 +10,22 @@ import { formatMonthParam, monthOf, previousMonth } from '../../src/domain/plann
|
||||
* plutôt qu'un bouton.
|
||||
*/
|
||||
|
||||
const MONTH = formatMonthParam(previousMonth(monthOf(new Date())));
|
||||
/**
|
||||
* Mois **courant**, et non le précédent.
|
||||
*
|
||||
* Le seed ne pose que deux semaines de planning : la courante et la
|
||||
* précédente. Le mois précédent n'en contient donc aucune, sauf quand on joue
|
||||
* la suite dans les premiers jours d'un mois — le tableau est vide le reste du
|
||||
* temps, et l'assertion échoue pour une raison sans rapport avec ce qu'elle
|
||||
* vérifie. Le défaut ne se voyait pas sur une base de développement, qui garde
|
||||
* les créneaux des exécutions précédentes ; l'intégration continue, elle,
|
||||
* repart d'un semis neuf.
|
||||
*
|
||||
* Le mois courant, lui, contient toujours aujourd'hui — donc toujours des
|
||||
* créneaux, et aucun n'a d'heures réelles puisque rien dans le semis ni dans la
|
||||
* suite n'en saisit.
|
||||
*/
|
||||
const MONTH = formatMonthParam(monthOf(new Date()));
|
||||
|
||||
/**
|
||||
* Mois propre à cette exécution.
|
||||
|
||||
Reference in new issue
Block a user