feat(deploy): generic orchestrator and deploy.sh entry

**Motivations:**
- Single generic orchestration in ia_dev while business logic stays in each project repo

**Root causes:**
- N/A (evolution)

**Correctifs:**
- N/A

**Evolutions:**
- Add orchestrator.sh (deploy.hooks.phases or fallback deploy.deploy_script_path)
- Add deploy.sh <project_id> <env> [options] as canonical entry from ia_dev root
- run-project-hooks.sh execs orchestrator.sh for backward compatibility
- change-to-all-branches.sh and deploy-by-script-to.sh invoke orchestrator.sh when IA_PROJECT_ID is set
- Document orchestration in README.md and deploy/lib/README.md

**Pages affectées:**
- README.md, deploy/orchestrator.sh, deploy/deploy.sh, deploy/run-project-hooks.sh, deploy/change-to-all-branches.sh, deploy/deploy-by-script-to.sh, deploy/lib/README.md
This commit is contained in:
Nicolas Cantu 2026-03-23 13:04:18 +01:00
parent 418bfb044a
commit 0a9d6e001b
7 changed files with 119 additions and 69 deletions

View File

@ -40,6 +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)** :
- **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).
- **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.
- **run-project-hooks.sh** : délègue à **orchestrator.sh** (alias pour compatibilité avec les anciennes invocations).
- **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>`.
- **pousse.sh** : build check, commit, push. Invocation : `./deploy/pousse.sh [project_id|--project <id>] [--remote <remote>] [--bump-version]` avec le message de commit sur STDIN. - **pousse.sh** : build check, commit, push. Invocation : `./deploy/pousse.sh [project_id|--project <id>] [--remote <remote>] [--bump-version]` avec le message de commit sur STDIN.

View File

@ -47,8 +47,8 @@ echo "[change-to-all-branches] Aligning branches..."
# scripts_v2 lives in the host project's deploy/ (not necessarily under ia_dev) # scripts_v2 lives in the host project's deploy/ (not necessarily under ia_dev)
DEPLOY_SCRIPTS_V2="${PROJECT_ROOT}/deploy/scripts_v2" DEPLOY_SCRIPTS_V2="${PROJECT_ROOT}/deploy/scripts_v2"
echo "[change-to-all-branches] Deploying test (--import-v1 --skipSetupHost, --no-sync-origin because we just pushed)..." echo "[change-to-all-branches] Deploying test (--import-v1 --skipSetupHost, --no-sync-origin because we just pushed)..."
if [[ -n "${IA_PROJECT_ID:-}" && -x "${DEPLOY_DIR}/run-project-hooks.sh" ]]; then if [[ -n "${IA_PROJECT_ID:-}" && -x "${DEPLOY_DIR}/orchestrator.sh" ]]; then
"${DEPLOY_DIR}/run-project-hooks.sh" test --import-v1 --skipSetupHost --no-sync-origin "${DEPLOY_DIR}/orchestrator.sh" test --import-v1 --skipSetupHost --no-sync-origin
else else
"${DEPLOY_SCRIPTS_V2}/deploy.sh" test --import-v1 --skipSetupHost --no-sync-origin "${DEPLOY_SCRIPTS_V2}/deploy.sh" test --import-v1 --skipSetupHost --no-sync-origin
fi fi

View File

@ -74,8 +74,8 @@ git fetch origin
git reset --hard "origin/${TARGET_BRANCH}" git reset --hard "origin/${TARGET_BRANCH}"
echo "[deploy-by-script-to] Step 4/5: deploy ${TARGET_BRANCH} (--import-v1 --skipSetupHost)..." echo "[deploy-by-script-to] Step 4/5: deploy ${TARGET_BRANCH} (--import-v1 --skipSetupHost)..."
if [[ -n "${IA_PROJECT_ID:-}" && -x "${DEPLOY_IA}/run-project-hooks.sh" ]]; then if [[ -n "${IA_PROJECT_ID:-}" && -x "${DEPLOY_IA}/orchestrator.sh" ]]; then
"${DEPLOY_IA}/run-project-hooks.sh" "$TARGET_BRANCH" --import-v1 --skipSetupHost "${DEPLOY_IA}/orchestrator.sh" "$TARGET_BRANCH" --import-v1 --skipSetupHost
else else
deploy_script="$PROJECT_ROOT/deploy/scripts_v2/deploy.sh" deploy_script="$PROJECT_ROOT/deploy/scripts_v2/deploy.sh"
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "${PROJECT_CONFIG_PATH:-}" ]] && command -v jq >/dev/null 2>&1; then if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "${PROJECT_CONFIG_PATH:-}" ]] && command -v jq >/dev/null 2>&1; then

27
deploy/deploy.sh Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Generic deploy entry from ia_dev: sets IA_PROJECT_ID and runs the 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...]
# Example: ./deploy/deploy.sh lecoffreio test --import-v1 --skipSetupHost
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}")"
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
if [[ $# -lt 2 ]]; then
echo "[deploy][ERROR] Missing arguments" >&2
echo "Usage: $0 <project_id> <env> [options passed to each phase / fallback deploy script]" >&2
echo "Example: $0 lecoffreio test --import-v1 --skipSetupHost" >&2
exit 1
fi
CONF="${IA_DEV_ROOT}/projects/${1}/conf.json"
if [[ ! -f "$CONF" ]]; then
echo "[deploy][ERROR] No conf for project '${1}': ${CONF}" >&2
exit 1
fi
export IA_PROJECT_ID="$1"
shift
exec "$DEPLOY_DIR/orchestrator.sh" "$@"

View File

@ -12,3 +12,8 @@ Optional `deploy_script_tee_log_if_requested <project_root> <log_subdir>` — re
## Policy ## Policy
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.
## 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`).
- **`orchestrator.sh`** : runs phases or fallback script; **`run-project-hooks.sh`** execs it for backward compatibility.

74
deploy/orchestrator.sh Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Generic deploy orchestrator: runs deploy.hooks.phases from projects/<id>/conf.json (paths relative to repository_root).
# If phases is empty or missing, exec deploy.deploy_script_path with the same arguments.
# Business logic (Prisma, systemd, remote layout) stays in each project's scripts under repository_root.
# Usage: orchestrator.sh <env> [options passed to each phase / fallback script]
# Requires: IA_PROJECT_ID, IA_DEV_ROOT (or re-exec from project root like change-to-all-branches).
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}")"
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
_ORCH_TAG="[orchestrator]"
if [[ -z "${IA_PROJECT_ID:-}" ]]; then
echo "${_ORCH_TAG}[ERROR] IA_PROJECT_ID is not set" >&2
exit 1
fi
# shellcheck source=../lib/project_config.sh
source "${IA_DEV_ROOT}/lib/project_config.sh"
# shellcheck source=../lib/project_git_root_from_conf.sh
source "${IA_DEV_ROOT}/lib/project_git_root_from_conf.sh"
ia_dev_resolve_project_git_root
REPO_ROOT="${IA_PROJECT_GIT_ROOT:-}"
if [[ -z "$REPO_ROOT" || ! -d "$REPO_ROOT" ]]; then
echo "${_ORCH_TAG}[ERROR] Could not resolve repository root for project ${IA_PROJECT_ID}" >&2
exit 1
fi
CONF="${PROJECT_CONFIG_PATH:-}"
if [[ -z "$CONF" || ! -f "$CONF" ]]; then
echo "${_ORCH_TAG}[ERROR] Missing conf: ${CONF:-}" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "${_ORCH_TAG}[ERROR] jq is required to read deploy.hooks.phases" >&2
exit 1
fi
SECRETS_PATH_CFG="$(jq -r '.deploy.secrets_path // empty' "$CONF")"
if [[ -n "$SECRETS_PATH_CFG" && "$SECRETS_PATH_CFG" != "null" && -d "$SECRETS_PATH_CFG" ]]; then
export SECRETS_BASE="$SECRETS_PATH_CFG"
export LECOFFRE_SECRETS_BASE="$SECRETS_PATH_CFG"
fi
DEPLOY_SCRIPT_PATH="$(jq -r '.deploy.deploy_script_path // empty' "$CONF")"
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
exit 1
fi
PHASE_COUNT="$(jq '.deploy.hooks.phases // [] | length' "$CONF")"
if [[ "$PHASE_COUNT" == "0" ]]; then
exec bash "$DEPLOY_SCRIPT_PATH" "$@"
fi
mapfile -t PHASE_SCRIPTS < <(jq -r '.deploy.hooks.phases[]? | if type == "string" then . elif type == "object" and (.run | type == "string") then .run else empty end' "$CONF")
if [[ ${#PHASE_SCRIPTS[@]} -eq 0 ]]; then
exec bash "$DEPLOY_SCRIPT_PATH" "$@"
fi
for rel in "${PHASE_SCRIPTS[@]}"; do
[[ -z "$rel" ]] && continue
phase_path="${REPO_ROOT}/${rel}"
if [[ ! -f "$phase_path" ]]; then
echo "${_ORCH_TAG}[ERROR] Phase script not found: ${phase_path}" >&2
exit 1
fi
echo "${_ORCH_TAG} Running: ${rel} $*"
bash "$phase_path" "$@"
done

View File

@ -1,71 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Run deploy.hooks.phases from projects/<id>/conf.json (paths relative to repository_root). # Backward-compatible alias: delegates to orchestrator.sh (generic deploy orchestrator).
# If phases is empty or missing, exec deploy.deploy_script_path with the same arguments.
# Usage: run-project-hooks.sh <env> [options passed to each phase / fallback script] # Usage: run-project-hooks.sh <env> [options passed to each phase / fallback script]
# Requires: IA_PROJECT_ID, IA_DEV_ROOT (or re-exec from project root like change-to-all-branches). # Requires: IA_PROJECT_ID (set by caller or by deploy.sh).
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}")"
DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)" DEPLOY_DIR="$(cd "$(dirname "$SCRIPT_REAL")" && pwd)"
IA_DEV_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)" exec "$DEPLOY_DIR/orchestrator.sh" "$@"
if [[ -z "${IA_PROJECT_ID:-}" ]]; then
echo "[run-project-hooks][ERROR] IA_PROJECT_ID is not set" >&2
exit 1
fi
# shellcheck source=../lib/project_config.sh
source "${IA_DEV_ROOT}/lib/project_config.sh"
# shellcheck source=../lib/project_git_root_from_conf.sh
source "${IA_DEV_ROOT}/lib/project_git_root_from_conf.sh"
ia_dev_resolve_project_git_root
REPO_ROOT="${IA_PROJECT_GIT_ROOT:-}"
if [[ -z "$REPO_ROOT" || ! -d "$REPO_ROOT" ]]; then
echo "[run-project-hooks][ERROR] Could not resolve repository root for project ${IA_PROJECT_ID}" >&2
exit 1
fi
CONF="${PROJECT_CONFIG_PATH:-}"
if [[ -z "$CONF" || ! -f "$CONF" ]]; then
echo "[run-project-hooks][ERROR] Missing conf: ${CONF:-}" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "[run-project-hooks][ERROR] jq is required to read deploy.hooks.phases" >&2
exit 1
fi
SECRETS_PATH_CFG="$(jq -r '.deploy.secrets_path // empty' "$CONF")"
if [[ -n "$SECRETS_PATH_CFG" && "$SECRETS_PATH_CFG" != "null" && -d "$SECRETS_PATH_CFG" ]]; then
export SECRETS_BASE="$SECRETS_PATH_CFG"
export LECOFFRE_SECRETS_BASE="$SECRETS_PATH_CFG"
fi
DEPLOY_SCRIPT_PATH="$(jq -r '.deploy.deploy_script_path // empty' "$CONF")"
if [[ -z "$DEPLOY_SCRIPT_PATH" || ! -f "$DEPLOY_SCRIPT_PATH" ]]; then
echo "[run-project-hooks][ERROR] deploy.deploy_script_path missing or not a file: ${DEPLOY_SCRIPT_PATH:-}" >&2
exit 1
fi
PHASE_COUNT="$(jq '.deploy.hooks.phases // [] | length' "$CONF")"
if [[ "$PHASE_COUNT" == "0" ]]; then
exec bash "$DEPLOY_SCRIPT_PATH" "$@"
fi
mapfile -t PHASE_SCRIPTS < <(jq -r '.deploy.hooks.phases[]? | if type == "string" then . elif type == "object" and (.run | type == "string") then .run else empty end' "$CONF")
if [[ ${#PHASE_SCRIPTS[@]} -eq 0 ]]; then
exec bash "$DEPLOY_SCRIPT_PATH" "$@"
fi
for rel in "${PHASE_SCRIPTS[@]}"; do
[[ -z "$rel" ]] && continue
phase_path="${REPO_ROOT}/${rel}"
if [[ ! -f "$phase_path" ]]; then
echo "[run-project-hooks][ERROR] Phase script not found: ${phase_path}" >&2
exit 1
fi
echo "[run-project-hooks] Running: ${rel} $*"
bash "$phase_path" "$@"
done