feat(deploy): methodology lib and project_orchestrator_path

**Motivations:**
- Keep shared methodology, envs, and future quality sequences in ia_dev; single project orchestrator script per repo

**Root causes:**
- N/A

**Correctifs:**
- N/A

**Evolutions:**
- Add deploy/lib/deploy-methodology.sh (test|pprod|prod validation)
- deploy.sh sources methodology before orchestrator
- orchestrator prefers deploy.project_orchestrator_path then legacy phases/deploy_script_path
- conf.json: project_orchestrator_path for lecoffreio, algo, enso; remove hooks where redundant
- Document in README.md, projects/README.md, deploy/lib/README.md

**Pages affectées:**
- deploy/*, projects/*/conf.json, README files
This commit is contained in:
Nicolas Cantu 2026-03-23 13:19:03 +01:00
parent 293b5ec9ba
commit f1c53477b0
9 changed files with 111 additions and 32 deletions

View File

@ -40,11 +40,12 @@ Tous les scripts sont invoqués depuis la **racine de ia_dev** (ce dépôt).
Les scripts dans `deploy/` **déploient et versionnent les projets configurés** (lecoffreio, enso, algo, etc.) dans leurs répertoires — ils ne déploient pas ia_dev. À lancer depuis la racine de ia_dev. Chaque script accepte en option un **project_id** en premier argument (ou `--project <id>` pour pousse.sh) pour cibler le projet ; sinon le projet est résolu par MAIL_TO ou AI_AGENT_TOKEN. Les chemins absolus dans `projects/<id>/conf.json` indiquent où se trouve chaque projet et ses scripts de déploiement. Les scripts dans `deploy/` **déploient et versionnent les projets configurés** (lecoffreio, enso, algo, etc.) dans leurs répertoires — ils ne déploient pas ia_dev. À lancer depuis la racine de ia_dev. Chaque script accepte en option un **project_id** en premier argument (ou `--project <id>` pour pousse.sh) pour cibler le projet ; sinon le projet est résolu par MAIL_TO ou AI_AGENT_TOKEN. Les chemins absolus dans `projects/<id>/conf.json` indiquent où se trouve chaque projet et ses scripts de déploiement.
**Orchestration générique (métier dans le dépôt cible)** : **Orchestration générique (méthodologie ia_dev → orchestrateur projet)** :
- **deploy.sh** : point dentrée `./deploy/deploy.sh <project_id> <env> [options…]` — exporte `IA_PROJECT_ID`, puis exécute **orchestrator.sh** (même contrat que les scripts ci-dessous qui invoquent lorchestrateur lorsque `IA_PROJECT_ID` est défini). - **deploy/lib/deploy-methodology.sh** : environnements autorisés (`test` \| `pprod` \| `prod`), validations communes ; évolutions « même méthodologie / qualité / séquences » pour tous les projets sajoutent ici (ou libs sœurs), pas dans les dépôts applicatifs.
- **orchestrator.sh** : lit `deploy.hooks.phases` dans `conf.json` (chemins relatifs à `repository_root`) ; si `phases` est vide, exécute `deploy.deploy_script_path`. Le métier (Prisma, systemd, build distant, etc.) reste dans les scripts du projet. - **deploy.sh** : `./deploy/deploy.sh <project_id> <env> [options…]` — méthodologie + `IA_PROJECT_ID`, puis **orchestrator.sh**.
- **run-project-hooks.sh** : délègue à **orchestrator.sh** (alias pour compatibilité avec les anciennes invocations). - **orchestrator.sh** : secrets depuis `conf.json`, puis **`exec`** du **script orchestrateur du dépôt cible** : `deploy.project_orchestrator_path` (relatif à `repository_root`). Un seul point dentrée côté projet ; ce script enchaîne les sous-scripts métier. **Repli** : sans `project_orchestrator_path`, ancien modèle `hooks.phases` ou `deploy.deploy_script_path`.
- **run-project-hooks.sh** : délègue à **orchestrator.sh** (compatibilité).
- **bump-version.sh** : lit `projects/<id>/conf.json` (version.package_json_paths, version.splash_app_name) et met à jour VERSION + package.json du **projet configuré**. Invocation : `./deploy/bump-version.sh [project_id] <version> [message]` depuis la racine de ia_dev. - **bump-version.sh** : lit `projects/<id>/conf.json` (version.package_json_paths, version.splash_app_name) et met à jour VERSION + package.json du **projet configuré**. Invocation : `./deploy/bump-version.sh [project_id] <version> [message]` depuis la racine de ia_dev.
- **deploy-by-script-to.sh** : checkout branche cible (pprod|prod), sync origin, exécute `deploy/scripts_v2/deploy.sh` du **projet configuré** (chemin dans conf), puis revient sur test. Invocation : `./deploy/deploy-by-script-to.sh [project_id] <pprod|prod>`. - **deploy-by-script-to.sh** : checkout branche cible (pprod|prod), sync origin, exécute `deploy/scripts_v2/deploy.sh` du **projet configuré** (chemin dans conf), puis revient sur test. Invocation : `./deploy/deploy-by-script-to.sh [project_id] <pprod|prod>`.

View File

@ -1,7 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Generic deploy entry from ia_dev: sets IA_PROJECT_ID and runs the orchestrator. # Generic deploy entry from ia_dev: shared methodology (envs, contract), then ia_dev orchestrator → project orchestrator.
# Business logic remains in the target repository (deploy_script_path, hooks.phases). # Usage (from ia_dev root): ./deploy/deploy.sh <project_id> <env> [options…]
# Usage (from ia_dev root): ./deploy/deploy.sh <project_id> <env> [options...]
# Example: ./deploy/deploy.sh lecoffreio test --import-v1 --skipSetupHost # Example: ./deploy/deploy.sh lecoffreio test --import-v1 --skipSetupHost
set -euo pipefail set -euo pipefail
@ -9,9 +8,12 @@ SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BA
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)" DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)" IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
# shellcheck source=lib/deploy-methodology.sh
source "${DEPLOY_DIR}/lib/deploy-methodology.sh"
if [[ $# -lt 2 ]]; then if [[ $# -lt 2 ]]; then
echo "[deploy][ERROR] Missing arguments" >&2 echo "[deploy][ERROR] Missing arguments" >&2
echo "Usage: $0 <project_id> <env> [options passed to each phase / fallback deploy script]" >&2 echo "Usage: $0 <project_id> <env> [options passed to project orchestrator]" >&2
echo "Example: $0 lecoffreio test --import-v1 --skipSetupHost" >&2 echo "Example: $0 lecoffreio test --import-v1 --skipSetupHost" >&2
exit 1 exit 1
fi fi
@ -22,6 +24,9 @@ if [[ ! -f "$CONF" ]]; then
exit 1 exit 1
fi fi
ia_dev_deploy_assert_env_literal "${2}" || exit 1
ia_dev_deploy_log_methodology_banner
export IA_PROJECT_ID="$1" export IA_PROJECT_ID="$1"
shift shift
exec "$DEPLOY_DIR/orchestrator.sh" "$@" exec "$DEPLOY_DIR/orchestrator.sh" "$@"

View File

@ -13,7 +13,15 @@ Optional `deploy_script_tee_log_if_requested <project_root> <log_subdir>` — re
Project-specific logic (Prisma, systemd unit names, remote app layout, LeCoffre domains) stays under each repositorys `deploy/scripts_v2/`. Only transport/logging helpers live here. Project-specific logic (Prisma, systemd unit names, remote app layout, LeCoffre domains) stays under each repositorys `deploy/scripts_v2/`. Only transport/logging helpers live here.
## `deploy-methodology.sh`
Shared contract for all managed projects: allowed envs (`test` \| `pprod` \| `prod`), validation helpers. Sourced by **`deploy.sh`** and **`orchestrator.sh`**. Extend only with an explicit decision (new env = conf + doc migration).
Quality gates and longer sequences that are identical across projects should be added here (or in small `deploy/lib/deploy-*.sh` peers) over time — not in project repos.
## Orchestration (`../orchestrator.sh`, `../deploy.sh`) ## Orchestration (`../orchestrator.sh`, `../deploy.sh`)
- **`deploy.sh`** : `./deploy/deploy.sh <project_id> <env> [args]` — generic entry; business scripts live in the target repo (`deploy_script_path` / `hooks.phases`). - **`deploy.sh`** : `./deploy/deploy.sh <project_id> <env> [args]` — applies methodology (env validation, banner), sets `IA_PROJECT_ID`, then **`exec orchestrator.sh`**.
- **`orchestrator.sh`** : runs phases or fallback script; **`run-project-hooks.sh`** execs it for backward compatibility. - **`orchestrator.sh`** : exports secrets from conf, then **`exec`** the **project orchestrator** `repository_root` + `deploy.project_orchestrator_path` (relative path) with the same `"$@"`. **Legacy** : if `project_orchestrator_path` is missing, uses `deploy.hooks.phases` or `deploy.deploy_script_path`. **`run-project-hooks.sh`** execs `orchestrator.sh` for backward compatibility.
Project orchestrator = single script in the target repo that sequences project-specific steps (`deploy.sh` LeCoffre today, or a dedicated `orchestrate-project.sh` later).

View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Shared deploy methodology for all ia_devmanaged projects: environments, quality bar, ordering contract.
# Sourced by ia_dev/deploy/deploy.sh and ia_dev/deploy/orchestrator.sh — no project-specific paths here.
# Project-specific sequencing lives only in the repository's project orchestrator (deploy.project_orchestrator_path).
# Environments are fixed across projects; extend only with an explicit decision and conf migration.
IA_DEV_DEPLOY_ENVS=(test pprod prod)
# ia_dev_deploy_env_is_allowed <word> — exit 0 if allowed
ia_dev_deploy_env_is_allowed() {
local e="${1:-}"
local x
for x in "${IA_DEV_DEPLOY_ENVS[@]}"; do
if [[ "$e" == "$x" ]]; then
return 0
fi
done
return 1
}
# ia_dev_deploy_assert_first_arg_env "$@" — first positional must be test|pprod|prod; stderr + exit 1 otherwise
ia_dev_deploy_assert_first_arg_env() {
if [[ $# -lt 1 ]]; then
echo "[ia_dev][deploy][ERROR] Missing <env> (expected: test | pprod | prod)" >&2
return 1
fi
local env_arg="$1"
ia_dev_deploy_assert_env_literal "$env_arg"
}
# ia_dev_deploy_assert_env_literal <env> — validate a single env token
ia_dev_deploy_assert_env_literal() {
local env_arg="${1:-}"
if [[ -z "$env_arg" ]]; then
echo "[ia_dev][deploy][ERROR] Missing <env> (expected: test | pprod | prod)" >&2
return 1
fi
if ! ia_dev_deploy_env_is_allowed "$env_arg"; then
echo "[ia_dev][deploy][ERROR] Invalid env '${env_arg}' (allowed: ${IA_DEV_DEPLOY_ENVS[*]})" >&2
return 1
fi
return 0
}
# ia_dev_deploy_log_methodology_banner — optional trace for support
ia_dev_deploy_log_methodology_banner() {
echo "[ia_dev][deploy] Methodology: envs={${IA_DEV_DEPLOY_ENVS[*]}} ; project orchestrator invoked after conf + secrets export (see orchestrator.sh)."
}

View File

@ -1,9 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Generic deploy orchestrator: runs deploy.hooks.phases from projects/<id>/conf.json (paths relative to repository_root). # ia_dev orchestrator: applies shared methodology, then invokes exactly one project orchestrator
# If phases is empty or missing, exec deploy.deploy_script_path with the same arguments. # (deploy.project_orchestrator_path relative to repository_root), which sequences project-specific scripts.
# Business logic (Prisma, systemd, remote layout) stays in each project's scripts under repository_root. # Legacy: if project_orchestrator_path is absent, falls back to deploy.hooks.phases or deploy.deploy_script_path.
# Usage: orchestrator.sh <env> [options passed to each phase / fallback script] # Usage: orchestrator.sh <env> [options…] — passed unchanged to the project orchestrator.
# Requires: IA_PROJECT_ID, IA_DEV_ROOT (or re-exec from project root like change-to-all-branches). # Requires: IA_PROJECT_ID, IA_DEV_ROOT (or callers like change-to-all-branches).
set -euo pipefail set -euo pipefail
SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")" SCRIPT_REAL="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo "${BASH_SOURCE[0]:-$0}")"
@ -12,11 +12,17 @@ IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
_ORCH_TAG="[orchestrator]" _ORCH_TAG="[orchestrator]"
# shellcheck source=lib/deploy-methodology.sh
source "${DEPLOY_DIR}/lib/deploy-methodology.sh"
if [[ -z "${IA_PROJECT_ID:-}" ]]; then if [[ -z "${IA_PROJECT_ID:-}" ]]; then
echo "${_ORCH_TAG}[ERROR] IA_PROJECT_ID is not set" >&2 echo "${_ORCH_TAG}[ERROR] IA_PROJECT_ID is not set" >&2
exit 1 exit 1
fi fi
ia_dev_deploy_assert_first_arg_env "$@" || exit 1
ia_dev_deploy_log_methodology_banner
# shellcheck source=../lib/project_config.sh # shellcheck source=../lib/project_config.sh
source "${IA_DEV_ROOT}/lib/project_config.sh" source "${IA_DEV_ROOT}/lib/project_config.sh"
# shellcheck source=../lib/project_git_root_from_conf.sh # shellcheck source=../lib/project_git_root_from_conf.sh
@ -35,7 +41,7 @@ if [[ -z "$CONF" || ! -f "$CONF" ]]; then
fi fi
if ! command -v jq >/dev/null 2>&1; then if ! command -v jq >/dev/null 2>&1; then
echo "${_ORCH_TAG}[ERROR] jq is required to read deploy.hooks.phases" >&2 echo "${_ORCH_TAG}[ERROR] jq is required to read deploy.* from conf.json" >&2
exit 1 exit 1
fi fi
@ -46,6 +52,22 @@ if [[ -n "$SECRETS_PATH_CFG" && "$SECRETS_PATH_CFG" != "null" && -d "$SECRETS_PA
fi fi
DEPLOY_SCRIPT_PATH="$(jq -r '.deploy.deploy_script_path // empty' "$CONF")" DEPLOY_SCRIPT_PATH="$(jq -r '.deploy.deploy_script_path // empty' "$CONF")"
PROJECT_ORCH_REL="$(jq -r '.deploy.project_orchestrator_path // empty' "$CONF")"
if [[ -n "$PROJECT_ORCH_REL" && "$PROJECT_ORCH_REL" != "null" ]]; then
PROJECT_ORCH_ABS="${REPO_ROOT}/${PROJECT_ORCH_REL}"
if [[ ! -f "$PROJECT_ORCH_ABS" ]]; then
echo "${_ORCH_TAG}[ERROR] deploy.project_orchestrator_path not a file: ${PROJECT_ORCH_ABS}" >&2
exit 1
fi
PHASE_NON_EMPTY="$(jq '.deploy.hooks.phases // [] | length' "$CONF")"
if [[ "$PHASE_NON_EMPTY" != "0" ]]; then
echo "${_ORCH_TAG}[WARN] deploy.hooks.phases is non-empty but project_orchestrator_path takes precedence; phases are ignored." >&2
fi
echo "${_ORCH_TAG} Project orchestrator: ${PROJECT_ORCH_REL} $*"
exec bash "$PROJECT_ORCH_ABS" "$@"
fi
if [[ -z "$DEPLOY_SCRIPT_PATH" || ! -f "$DEPLOY_SCRIPT_PATH" ]]; then if [[ -z "$DEPLOY_SCRIPT_PATH" || ! -f "$DEPLOY_SCRIPT_PATH" ]]; then
echo "${_ORCH_TAG}[ERROR] deploy.deploy_script_path missing or not a file: ${DEPLOY_SCRIPT_PATH:-}" >&2 echo "${_ORCH_TAG}[ERROR] deploy.deploy_script_path missing or not a file: ${DEPLOY_SCRIPT_PATH:-}" >&2
exit 1 exit 1
@ -53,6 +75,7 @@ fi
PHASE_COUNT="$(jq '.deploy.hooks.phases // [] | length' "$CONF")" PHASE_COUNT="$(jq '.deploy.hooks.phases // [] | length' "$CONF")"
if [[ "$PHASE_COUNT" == "0" ]]; then if [[ "$PHASE_COUNT" == "0" ]]; then
echo "${_ORCH_TAG}[WARN] deploy.project_orchestrator_path unset; falling back to deploy_script_path only." >&2
exec bash "$DEPLOY_SCRIPT_PATH" "$@" exec bash "$DEPLOY_SCRIPT_PATH" "$@"
fi fi
@ -69,6 +92,6 @@ for rel in "${PHASE_SCRIPTS[@]}"; do
echo "${_ORCH_TAG}[ERROR] Phase script not found: ${phase_path}" >&2 echo "${_ORCH_TAG}[ERROR] Phase script not found: ${phase_path}" >&2
exit 1 exit 1
fi fi
echo "${_ORCH_TAG} Running: ${rel} $*" echo "${_ORCH_TAG} Running (legacy phases): ${rel} $*"
bash "$phase_path" "$@" bash "$phase_path" "$@"
done done

View File

@ -45,7 +45,9 @@ One JSON file per project: `projects/<id>/conf.json` (e.g. `projects/lecoffreio/
| `project_path` | no | **Absolute** path to the project deploy root (e.g. `/home/desk/code/lecoffre_ng_test/deploy`). | | `project_path` | no | **Absolute** path to the project deploy root (e.g. `/home/desk/code/lecoffre_ng_test/deploy`). |
| `build_dirs` | no | **Absolute** paths to directories where `npm run build` is run. | | `build_dirs` | no | **Absolute** paths to directories where `npm run build` is run. |
| `deploy.scripts_path` | no | **Absolute** path to deploy scripts (e.g. `…/deploy/scripts_v2`). | | `deploy.scripts_path` | no | **Absolute** path to deploy scripts (e.g. `…/deploy/scripts_v2`). |
| `deploy.deploy_script_path` | no | **Absolute** path to deploy.sh. | | `deploy.deploy_script_path` | no | **Absolute** path to deploy.sh (fallback / tooling; keep aligned with project orchestrator). |
| `deploy.project_orchestrator_path` | recommended | **Relative to `deploy.repository_root`** : single project orchestrator script (sequences project-specific deploy steps). Preferred over `deploy.hooks.phases`. |
| `deploy.hooks.phases` | legacy | Optional list of scripts relative to `repository_root`; used only if `project_orchestrator_path` is unset. |
| `deploy.secrets_path` | no | **Absolute** path to the projects `.secrets` directory. | | `deploy.secrets_path` | no | **Absolute** path to the projects `.secrets` directory. |
| `version.package_json_paths` | no | **Absolute** paths to package.json files to bump. | | `version.package_json_paths` | no | **Absolute** paths to package.json files to bump. |
| `mail.imap_bridge_env` | no | **Relative to ia_dev root**: path to IMAP bridge env file (e.g. `.secrets/gitea-issues/imap-bridge.env`). | | `mail.imap_bridge_env` | no | **Relative to ia_dev root**: path to IMAP bridge env file (e.g. `.secrets/gitea-issues/imap-bridge.env`). |

View File

@ -11,10 +11,8 @@
"repository_root": "/home/desk/code/algo", "repository_root": "/home/desk/code/algo",
"scripts_path": "/home/desk/code/algo/deploy/scripts_v2", "scripts_path": "/home/desk/code/algo/deploy/scripts_v2",
"deploy_script_path": "/home/desk/code/algo/deploy/scripts_v2/deploy.sh", "deploy_script_path": "/home/desk/code/algo/deploy/scripts_v2/deploy.sh",
"secrets_path": "/home/desk/code/algo/.secrets", "project_orchestrator_path": "deploy/scripts_v2/deploy.sh",
"hooks": { "secrets_path": "/home/desk/code/algo/.secrets"
"phases": []
}
}, },
"version": { "version": {
"package_json_paths": [ "package_json_paths": [

View File

@ -11,10 +11,8 @@
"repository_root": "/home/desk/code/enso", "repository_root": "/home/desk/code/enso",
"scripts_path": "/home/desk/code/enso/deploy/scripts_v2", "scripts_path": "/home/desk/code/enso/deploy/scripts_v2",
"deploy_script_path": "/home/desk/code/enso/deploy/scripts_v2/deploy.sh", "deploy_script_path": "/home/desk/code/enso/deploy/scripts_v2/deploy.sh",
"secrets_path": "/home/desk/code/enso/.secrets", "project_orchestrator_path": "deploy/scripts_v2/deploy.sh",
"hooks": { "secrets_path": "/home/desk/code/enso/.secrets"
"phases": []
}
}, },
"version": { "version": {
"package_json_paths": [ "package_json_paths": [

View File

@ -11,12 +11,8 @@
"repository_root": "/home/desk/code/lecoffre_ng_test", "repository_root": "/home/desk/code/lecoffre_ng_test",
"scripts_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2", "scripts_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2",
"deploy_script_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2/deploy.sh", "deploy_script_path": "/home/desk/code/lecoffre_ng_test/deploy/scripts_v2/deploy.sh",
"secrets_path": "/home/desk/code/ia_dev/projects/lecoffreio/.secrets", "project_orchestrator_path": "deploy/scripts_v2/deploy.sh",
"hooks": { "secrets_path": "/home/desk/code/ia_dev/projects/lecoffreio/.secrets"
"phases": [
"deploy/scripts_v2/deploy.sh"
]
}
}, },
"version": { "version": {
"package_json_paths": [ "package_json_paths": [