feat: resolve project conf paths relative to smart_ide monorepo root

- Add lib/conf_path_resolve.sh (canonical conf under projects/ or ia_dev/projects/)
- Apply resolution in project_git_root_from_conf, deploy-conf-handling, orchestrator
- pousse: monorepo-relative build_dirs when path starts with ../
- deploy-by-script-to: resolve secrets_path and deploy_script_path
- Sync smart_ide/enso conf.json with relative paths; document in projects/README
This commit is contained in:
Nicolas Cantu 2026-04-03 18:03:55 +02:00
parent 756bab0cd8
commit a7b5998f61
10 changed files with 158 additions and 28 deletions

View File

@ -7,7 +7,7 @@ Dépôt de pilotage par l'IA pour les projets : **équipe d'agents IA** dont le
## Usage (standalone)
- **Racine d'exécution** : tous les scripts sont lancés depuis la **racine de ia_dev** (ce dépôt). L'id projet est résolu uniquement par **MAIL_TO** ou **AI_AGENT_TOKEN** (voir `projects/README.md`).
- **Config** : dans `projects/<id>/conf.json`, les champs `project_path`, `build_dirs`, `deploy.scripts_path`, `deploy.deploy_script_path`, `deploy.secrets_path`, `version.package_json_paths` sont des **chemins absolus** (vers les dépôts des projets). Les champs `mail.imap_bridge_env` et `git.token_file` sont **relatifs à la racine de ia_dev**. Le répertoire `.secrets` à la racine de ia_dev contient `token` et `gitea-issues/agent-loop.env`, `gitea-issues/imap-bridge.env`.
- **Config** : dans `projects/<id>/conf.json`, les chemins vers les dépôts projet peuvent être **absolus** ou **relatifs à la racine du monorepo smart_ide** lorsque ia_dev y est intégré (voir `lib/conf_path_resolve.sh`, `projects/README.md` amont). Les champs `mail.imap_bridge_env` et `git.token_file` sont **relatifs à la racine de ia_dev**. Le répertoire `.secrets` à la racine de ia_dev contient `token` et `gitea-issues/agent-loop.env`, `gitea-issues/imap-bridge.env`.
Voir `projects/README.md` pour le schéma de configuration et les exemples.

View File

@ -29,6 +29,7 @@ if [[ -z "$PROJECT_ROOT" || ! -d "$PROJECT_ROOT" ]]; then
fi
if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "${PROJECT_CONFIG_PATH:-}" ]] && command -v jq >/dev/null 2>&1; then
_sp="$(jq -r '.deploy.secrets_path // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)"
_sp="$(ia_dev_resolve_path_from_conf "$PROJECT_CONFIG_PATH" "$_sp")"
if [[ -n "$_sp" && "$_sp" != "null" && -d "$_sp" ]]; then
export SECRETS_BASE="$_sp"
export LECOFFRE_SECRETS_BASE="$_sp"
@ -106,6 +107,7 @@ else
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
_cfg_script="$(jq -r '.deploy.deploy_script_path // ""' "$PROJECT_CONFIG_PATH" 2>/dev/null)"
_cfg_script="$(ia_dev_resolve_path_from_conf "$PROJECT_CONFIG_PATH" "$_cfg_script")"
[[ -n "$_cfg_script" && -x "$_cfg_script" ]] && deploy_script="$_cfg_script"
fi
"$deploy_script" "$TARGET_BRANCH"

View File

@ -3,6 +3,10 @@
# Add here any new deploy.* field that must be read the same way for all projects.
# Do not put project-specific paths, hostnames, or phase ordering here beyond generic keys.
_DEPLOY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../../lib/conf_path_resolve.sh
source "${_DEPLOY_LIB_DIR}/../../lib/conf_path_resolve.sh"
# ia_dev_deploy_require_jq <log_tag> — exit 1 if jq missing (e.g. log_tag="[orchestrator]")
ia_dev_deploy_require_jq() {
local tag="${1:-[ia_dev][deploy]}"
@ -17,6 +21,7 @@ ia_dev_deploy_secrets_export_from_conf() {
local conf="${1:?}"
local secrets_path
secrets_path="$(jq -r '.deploy.secrets_path // empty' "$conf")"
secrets_path="$(ia_dev_resolve_path_from_conf "$conf" "$secrets_path")"
if [[ -n "$secrets_path" && "$secrets_path" != "null" && -d "$secrets_path" ]]; then
export SECRETS_BASE="$secrets_path"
export LECOFFRE_SECRETS_BASE="$secrets_path"

View File

@ -52,6 +52,7 @@ ia_dev_deploy_export_runtime_context "$REPO_ROOT" "${1:-}"
ia_dev_deploy_assert_handoff_context "$REPO_ROOT" "${1:-}" "${_ORCH_TAG}"
DEPLOY_SCRIPT_PATH="$(jq -r '.deploy.deploy_script_path // empty' "$CONF")"
DEPLOY_SCRIPT_PATH="$(ia_dev_resolve_path_from_conf "$CONF" "$DEPLOY_SCRIPT_PATH")"
PROJECT_ORCH_REL="$(jq -r '.deploy.project_orchestrator_path // empty' "$CONF")"
if [[ -n "$PROJECT_ORCH_REL" && "$PROJECT_ORCH_REL" != "null" ]]; then

View File

@ -131,12 +131,16 @@ if [[ -n "${PROJECT_CONFIG_PATH:-}" && -f "$PROJECT_CONFIG_PATH" ]] && command -
done < <(jq -r '.build_dirs[]? // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null)
fi
if [[ ${#build_dirs[@]} -gt 0 ]]; then
# shellcheck source=../lib/conf_path_resolve.sh
source "${IA_DEV_ROOT}/lib/conf_path_resolve.sh"
echo "[pousse] Build check (${#build_dirs[@]} dirs from project config)..."
for dir in "${build_dirs[@]}"; do
if [[ "$dir" = /* ]]; then
abs_dir="$dir"
elif [[ "$dir" == ../* || "$dir" == ".." ]]; then
abs_dir="$(ia_dev_resolve_path_from_conf "$PROJECT_CONFIG_PATH" "$dir")"
else
abs_dir="${repo_root}/${dir}"
abs_dir="${git_work_root}/${dir}"
fi
if [[ ! -d "$abs_dir" ]]; then
echo "[pousse][WARN] Skipping build ${dir} (directory not found)" >&2

48
lib/conf_path_resolve.sh Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Resolve paths in projects/<id>/conf.json: absolute paths unchanged; others relative to smart_ide monorepo root.
# Monorepo root is the directory that contains ./projects/ (conf at .../projects/<id>/conf.json) or, when the
# file lives under .../ia_dev/projects/<id>/conf.json, the parent of ./ia_dev/.
# ia_dev_smart_ide_monorepo_root_from_conf <path_to_conf.json>
ia_dev_smart_ide_monorepo_root_from_conf() {
local conf="${1:?}"
local c="$conf"
if command -v realpath >/dev/null 2>&1; then
c="$(realpath "$conf" 2>/dev/null)" || c="$conf"
else
c="$(readlink -f "$conf" 2>/dev/null)" || c="$conf"
fi
local d
d="$(dirname "$c")"
if [[ "$c" == */ia_dev/projects/*/conf.json ]]; then
( cd "$d/../../.." && pwd )
return
fi
if [[ "$c" == */projects/*/conf.json ]]; then
( cd "$d/../.." && pwd )
return
fi
( cd "$d/../.." && pwd )
}
# ia_dev_resolve_path_from_conf <path_to_conf.json> <path_field>
ia_dev_resolve_path_from_conf() {
local conf="${1:?}"
local p="${2:-}"
p="${p//$'\r'/}"
if [[ -z "$p" || "$p" == "null" ]]; then
printf '%s\n' ""
return 0
fi
if [[ "$p" = /* ]]; then
printf '%s\n' "$p"
return 0
fi
local root
root="$(ia_dev_smart_ide_monorepo_root_from_conf "$conf")"
if ( cd "$root" && realpath -m "$p" >/dev/null 2>&1 ); then
( cd "$root" && realpath -m "$p" )
else
printf '%s\n' "$root/$p"
fi
}

View File

@ -7,9 +7,14 @@
# 2. deploy.git_work_tree (alias)
# 3. dirname(deploy.secrets_path) — legacy; breaks if secrets live outside the repo tree
#
# Paths may be absolute or relative to the smart_ide monorepo root (see conf_path_resolve.sh).
#
# Preconditions: PROJECT_CONFIG_PATH set and jq available.
# Sets: IA_PROJECT_GIT_ROOT (exported), or empty if unresolved.
#
# shellcheck source=conf_path_resolve.sh
source "$(dirname "${BASH_SOURCE[0]}")/conf_path_resolve.sh"
ia_dev_resolve_project_git_root() {
IA_PROJECT_GIT_ROOT=""
export IA_PROJECT_GIT_ROOT
@ -19,18 +24,20 @@ ia_dev_resolve_project_git_root() {
if ! command -v jq >/dev/null 2>&1; then
return 0
fi
local r sp
local r sp r_abs sp_abs
r="$(jq -r '.deploy.repository_root // .deploy.git_work_tree // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null || true)"
r="${r//$'\r'/}"
if [[ -n "$r" && "$r" != "null" && -d "$r" ]]; then
IA_PROJECT_GIT_ROOT="$r"
r_abs="$(ia_dev_resolve_path_from_conf "$PROJECT_CONFIG_PATH" "$r")"
if [[ -n "$r_abs" && -d "$r_abs" ]]; then
IA_PROJECT_GIT_ROOT="$r_abs"
export IA_PROJECT_GIT_ROOT
return 0
fi
sp="$(jq -r '.deploy.secrets_path // empty' "$PROJECT_CONFIG_PATH" 2>/dev/null || true)"
sp="${sp//$'\r'/}"
if [[ -n "$sp" && "$sp" != "null" ]]; then
IA_PROJECT_GIT_ROOT="$(dirname "$sp")"
sp_abs="$(ia_dev_resolve_path_from_conf "$PROJECT_CONFIG_PATH" "$sp")"
if [[ -n "$sp_abs" && "$sp_abs" != "null" ]]; then
IA_PROJECT_GIT_ROOT="$(dirname "$sp_abs")"
export IA_PROJECT_GIT_ROOT
fi
}

View File

@ -5,7 +5,9 @@ This repo (`ia_dev`) is a **standalone** depot. Project-specific parameters are
**Paths in conf.json:**
The scripts in `deploy/` deploy and build the **configured projects** (lecoffreio, enso, algo, etc.) in their own directories; they do not deploy ia_dev.
- **Absolute** (required): `project_path`, `build_dirs`, `deploy.scripts_path`, `deploy.deploy_script_path`, `deploy.secrets_path`, `version.package_json_paths` — they point to each project repo.
- **Project / deploy paths** (`project_path`, `deploy.repository_root`, `deploy.deploy_script_path`, `deploy.secrets_path`, `deploy.scripts_path`): **absolute** or **relative to the smart_ide monorepo root** (parent of `projects/` and `ia_dev/`). Resolution uses `lib/conf_path_resolve.sh` when the config file lives under `…/projects/<id>/conf.json` or `…/ia_dev/projects/<id>/conf.json`.
- **`build_dirs`**: each entry is either **absolute**, **relative to the monorepo root** if it starts with `../`, or **relative to the project git root** (`repository_root`) otherwise (e.g. `enso/enso-front`).
- **`version.package_json_paths`**: **relative to the project git root** unless absolute (e.g. `package.json`, `enso/enso-front/package.json`).
- **Relative to ia_dev root** (this project): `mail.imap_bridge_env`, `git.token_file`. They point to files under ia_devs own `.secrets/`.
## Current project selection
@ -23,7 +25,7 @@ The token in the request (e.g. Bearer) is matched by scanning all projects and a
Scripts use **parameter** (or **`IA_PROJECT_ID`** env set by scripts), **`MAIL_TO`** (env) or **`AI_AGENT_TOKEN`** (env); they set `PROJECT_ID` and, for token, `PROJECT_ENV`. No other source (no `IA_PROJECT`, no `ai_project_id`, no `.ia_project`).
**Usage unique : standalone.** Tous les scripts sont lancés depuis la racine de ia_dev. Les chemins dans conf sont absolus (sauf `imap_bridge_env` et `token_file`, relatifs à la racine de ia_dev).
**Usage :** scripts lancés depuis la racine de ia_dev. Les chemins projet peuvent être absolus ou relatifs à la racine du monorepo **smart_ide** lorsque ia_dev y est intégré ; `mail.*` / `git.token_file` restent relatifs à la racine de ia_dev.
## Rule: conf.json is read-only for agents
@ -42,15 +44,15 @@ One JSON file per project: `projects/<id>/conf.json` (e.g. `projects/lecoffreio/
|-------|----------|-------------|
| `id` | no | Project identifier (directory name under `projects/`); default: directory name. |
| `name` | no | Human-readable project name. |
| `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. |
| `deploy.scripts_path` | no | **Absolute** path to deploy scripts (e.g. `…/deploy/scripts_v2`). |
| `deploy.deploy_script_path` | no | **Absolute** path to deploy.sh (fallback / tooling; keep aligned with project orchestrator). |
| `project_path` | no | Git clone root for cron / tooling: **absolute** or **relative to smart_ide monorepo root** (e.g. `../enso`, `.`). |
| `build_dirs` | no | `npm run build` dirs: **absolute**; **monorepo-relative** if value starts with `../`; else **relative to repository root**. |
| `deploy.scripts_path` | no | Deploy scripts dir: **absolute** or **monorepo-relative** (e.g. `../enso/deploy/scripts_v2`). |
| `deploy.deploy_script_path` | no | **Absolute** or **monorepo-relative** path to `deploy.sh` (fallback / tooling). |
| `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** or **monorepo-relative** path to the projects `.secrets` directory. |
| `deploy.host_stays_on_test` | no | If **true**, `deploy-by-script-to.sh` keeps the app clone on branch **test** for **pprod**/**prod** deploys (no `checkout`/`reset --hard` on those branches); the projects `deploy.sh` aligns remotes / worktree. **false** or omitted: legacy behaviour (checkout target branch, sync, deploy, checkout **test**). LeCoffre sets **true** permanently in `projects/lecoffreio/conf.json`. |
| `version.package_json_paths` | no | **Absolute** paths to package.json files to bump. |
| `version.package_json_paths` | no | Paths to `package.json` for version bump: **absolute** or **relative to repository root**. |
| `mail.imap_bridge_env` | no | **Relative to ia_dev root**: path to IMAP bridge env file (e.g. `.secrets/gitea-issues/imap-bridge.env`). |
| `git.token_file` | no | **Relative to ia_dev root**: path to Gitea token file (e.g. `.secrets/gitea-issues/token`). |

View File

@ -1,23 +1,25 @@
{
"id": "enso",
"name": "enso",
"project_path": "/home/desk/code/enso/deploy",
"cron": {
"git_pull": true
},
"project_path": "../enso",
"build_dirs": [
"/home/desk/code/enso/deploy/lecoffre-ressources-dev",
"/home/desk/code/enso/deploy/lecoffre-back-main",
"/home/desk/code/enso/deploy/lecoffre-front-main"
".",
"enso/enso-front"
],
"deploy": {
"repository_root": "/home/desk/code/enso",
"scripts_path": "/home/desk/code/enso/deploy/scripts_v2",
"deploy_script_path": "/home/desk/code/enso/deploy/scripts_v2/deploy.sh",
"repository_root": "../enso",
"scripts_path": "../enso/deploy/scripts_v2",
"deploy_script_path": "../enso/deploy/scripts_v2/deploy.sh",
"project_orchestrator_path": "deploy/scripts_v2/deploy.sh",
"secrets_path": "/home/desk/code/enso/.secrets"
"secrets_path": "../enso/.secrets"
},
"version": {
"package_json_paths": [
"/home/desk/code/enso/deploy/lecoffre-back-main/package.json",
"/home/desk/code/enso/deploy/lecoffre-front-main/package.json"
"package.json",
"enso/enso-front/package.json"
],
"splash_app_name": "enso"
},
@ -25,11 +27,11 @@
"imap_bridge_env": ".secrets/gitea-issues/imap-bridge.env"
},
"git": {
"wiki_url": "https://git.4nkweb.com/nicolas.cantu/enso/wiki",
"wiki_url": "https://git.4nkweb.com/4nk/enso/wiki",
"token_file": ".secrets/gitea-issues/token"
},
"tickets": {
"ticketing_url": "https://git.4nkweb.com/nicolas.cantu/enso/issues",
"ticketing_url": "https://git.4nkweb.com/4nk/enso/issues",
"authorized_emails": {
"to": [
{
@ -40,5 +42,43 @@
],
"from": ["nicolas.4nk@pm.me"]
}
},
"smart_ide": {
"remote_data_access": {
"environments": {
"test": {
"ssh_host_alias": "enso-test-app",
"remote_data_directories": [
{
"role": "docv_storage",
"path_on_server": "/var/lib/enso/test/data"
}
]
},
"pprod": {
"ssh_host_alias": "enso-pprod-app",
"remote_data_directories": [
{
"role": "docv_storage",
"path_on_server": "/var/lib/enso/pprod/data"
}
]
},
"prod": {
"ssh_host_alias": "enso-prod-app",
"remote_data_directories": [
{
"role": "docv_storage",
"path_on_server": "/var/lib/enso/prod/data"
}
]
}
}
},
"anythingllm_workspace_slug": {
"test": "enso-test",
"pprod": "enso-pprod",
"prod": "enso-prod"
}
}
}

View File

@ -1,7 +1,10 @@
{
"id": "smart_ide",
"name": "smart_ide",
"project_path": "/home/ncantu/code/smart_ide",
"cron": {
"git_pull": true
},
"project_path": ".",
"build_dirs": [],
"deploy": {},
"version": {
@ -27,5 +30,23 @@
],
"from": ["nicolas.4nk@pm.me"]
}
},
"smart_ide": {
"remote_data_access": {
"environments": {
"test": {
"ssh_host_alias": "smart-ide-test",
"remote_data_directories": []
},
"pprod": {
"ssh_host_alias": "smart-ide-pprod",
"remote_data_directories": []
},
"prod": {
"ssh_host_alias": "smart-ide-prod",
"remote_data_directories": []
}
}
}
}
}