mirror of
https://github.com/go-task/task.git
synced 2026-06-11 09:51:50 +00:00
chore: add scoped variables planning documents (to be reverted)
This commit is contained in:
214
PLAN_SCOPED_VARIABLES.md
Normal file
214
PLAN_SCOPED_VARIABLES.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Plan : Scoped Variables pour les Includes de Taskfiles
|
||||
|
||||
## Objectif
|
||||
|
||||
Implémenter le scoping des variables pour les Taskfiles inclus. Les variables d'un include seront accessibles via `{{.namespace.VAR}}` au lieu d'être mergées globalement (problème actuel de conflits).
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- **Accès** : Préfixe namespace (`{{.db.HOST}}`)
|
||||
- **Conflits** : Préfixage obligatoire, pas de merge global
|
||||
- **Requires/Preconditions** : Reporté à une phase ultérieure
|
||||
|
||||
---
|
||||
|
||||
## Visibilité des variables
|
||||
|
||||
| Relation | Accès | Mécanisme |
|
||||
|----------|-------|-----------|
|
||||
| Parent → Include | `{{.ns.VAR}}` | Le but principal |
|
||||
| Include → Parent | Non (via `includes.vars:` si besoin) | Isolation pour réutilisabilité |
|
||||
| Include → Sibling | Non (via parent comme passeur) | Évite les dépendances implicites |
|
||||
| Include → Soi-même | `{{.VAR}}` | Naturel |
|
||||
|
||||
**Exemple de passage explicite du parent vers l'include :**
|
||||
```yaml
|
||||
includes:
|
||||
db:
|
||||
taskfile: ./db/Taskfile.yml
|
||||
vars:
|
||||
HOST: "{{.MY_HOST}}" # Passage explicite des vars nécessaires
|
||||
```
|
||||
|
||||
**Exemple de passage entre siblings via le parent :**
|
||||
```yaml
|
||||
includes:
|
||||
a: ./a/Taskfile.yml
|
||||
b:
|
||||
taskfile: ./b/Taskfile.yml
|
||||
vars:
|
||||
A_VAR: "{{.a.SOME_VAR}}" # Parent passe les vars de a à b
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 1 : Ajouter l'experiment flag
|
||||
|
||||
**Fichier** : `experiments/experiments.go`
|
||||
|
||||
```go
|
||||
var (
|
||||
GentleForce Experiment
|
||||
RemoteTaskfiles Experiment
|
||||
EnvPrecedence Experiment
|
||||
ScopedVariables Experiment // NEW
|
||||
)
|
||||
|
||||
func ParseWithConfig(dir string, config *ast.TaskRC) {
|
||||
// ...
|
||||
ScopedVariables = New("SCOPED_VARIABLES", config, 1)
|
||||
}
|
||||
```
|
||||
|
||||
Activation : `TASK_X_SCOPED_VARIABLES=1`
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 : Modifier la structure `Vars`
|
||||
|
||||
**Fichier** : `taskfile/ast/vars.go`
|
||||
|
||||
Ajouter un champ pour stocker les variables scopées par namespace :
|
||||
|
||||
```go
|
||||
type Vars struct {
|
||||
om *orderedmap.OrderedMap[string, Var]
|
||||
mutex sync.RWMutex
|
||||
scoped map[string]*Vars // NEW: namespace -> vars
|
||||
scopedMutex sync.RWMutex
|
||||
}
|
||||
```
|
||||
|
||||
Nouvelles méthodes à ajouter :
|
||||
|
||||
```go
|
||||
// SetScoped stocke les variables d'un namespace
|
||||
func (vars *Vars) SetScoped(namespace string, scopedVars *Vars)
|
||||
|
||||
// GetScoped récupère les variables d'un namespace
|
||||
func (vars *Vars) GetScoped(namespace string) (*Vars, bool)
|
||||
|
||||
// MergeScoped merge les variables dans un namespace au lieu de globalement
|
||||
func (vars *Vars) MergeScoped(namespace string, other *Vars, include *Include)
|
||||
```
|
||||
|
||||
Modifier `ToCacheMap()` pour inclure les namespaces comme maps imbriquées :
|
||||
|
||||
```go
|
||||
func (vars *Vars) ToCacheMap() map[string]any {
|
||||
m := make(map[string]any, vars.Len())
|
||||
// Variables plates existantes
|
||||
for k, v := range vars.All() { ... }
|
||||
|
||||
// NEW: Ajouter les namespaces comme maps imbriquées
|
||||
for namespace, scopedVars := range vars.scoped {
|
||||
m[namespace] = scopedVars.ToCacheMap()
|
||||
}
|
||||
return m
|
||||
}
|
||||
```
|
||||
|
||||
Cela permet `{{.db.HOST}}` naturellement via Go templates.
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 : Modifier `Taskfile.Merge()`
|
||||
|
||||
**Fichier** : `taskfile/ast/taskfile.go`
|
||||
|
||||
```go
|
||||
func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
|
||||
// ... validations existantes ...
|
||||
|
||||
if experiments.ScopedVariables.Enabled() && include != nil && !include.Flatten {
|
||||
// Scoped merge : variables dans le namespace
|
||||
t1.Vars.MergeScoped(include.Namespace, t2.Vars, include)
|
||||
t1.Env.MergeScoped(include.Namespace, t2.Env, include)
|
||||
} else {
|
||||
// Comportement legacy : merge plat
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
}
|
||||
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 : Accès local sans préfixe pour les tasks de l'include
|
||||
|
||||
**Fichier** : `compiler.go`
|
||||
|
||||
Dans `getVariables()`, ajouter les variables du namespace courant pour qu'une task de l'include puisse y accéder sans préfixe :
|
||||
|
||||
```go
|
||||
// Si la task a un namespace, ajouter ses variables localement
|
||||
if experiments.ScopedVariables.Enabled() && t != nil && t.Namespace != "" {
|
||||
if scopedVars, ok := c.TaskfileVars.GetScoped(t.Namespace); ok {
|
||||
for k, v := range scopedVars.All() {
|
||||
result.Set(k, v) // Accessible via {{.VAR}} dans le namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 5 : Gérer les includes imbriqués
|
||||
|
||||
**Fichier** : `taskfile/ast/graph.go`
|
||||
|
||||
Pour les includes imbriqués (a→b→c), propager le path complet du namespace :
|
||||
- `{{.a.VAR}}` - variable de a
|
||||
- `{{.a.b.VAR}}` - variable de b (inclus par a)
|
||||
|
||||
Option : ajouter `FullNamespacePath []string` dans `Include` ou calculer lors du merge.
|
||||
|
||||
---
|
||||
|
||||
## Étape 6 : Gérer le cas `flatten: true`
|
||||
|
||||
Déjà couvert par la condition `!include.Flatten` dans l'étape 3. Les includes aplatis conservent le comportement actuel (merge global).
|
||||
|
||||
---
|
||||
|
||||
## Étape 7 : Tests
|
||||
|
||||
**Nouveau fichier** : `testdata/includes/scoped_vars/`
|
||||
|
||||
Scénarios de test :
|
||||
1. **Accès cross-namespace** : `{{.db.HOST}}` depuis le Taskfile parent
|
||||
2. **Accès local** : `{{.HOST}}` depuis une task de l'include `db`
|
||||
3. **Override via include.vars** : Les vars passées dans `includes:` continuent de fonctionner
|
||||
4. **Pas de conflits** : Deux includes avec même nom de variable ne se marchent pas dessus
|
||||
5. **Flatten** : `flatten: true` conserve le merge global
|
||||
6. **Includes imbriqués** : `{{.a.b.VAR}}`
|
||||
|
||||
---
|
||||
|
||||
## Fichiers à modifier
|
||||
|
||||
| Fichier | Changement |
|
||||
|---------|------------|
|
||||
| `experiments/experiments.go` | Ajouter `ScopedVariables` experiment |
|
||||
| `taskfile/ast/vars.go` | Ajouter `scoped` map, `MergeScoped()`, modifier `ToCacheMap()` |
|
||||
| `taskfile/ast/taskfile.go` | Conditionner le merge scoped/plat |
|
||||
| `compiler.go` | Injecter les vars du namespace dans le scope local de la task |
|
||||
| `taskfile/ast/graph.go` | (optionnel) Propager le path namespace pour les includes imbriqués |
|
||||
|
||||
---
|
||||
|
||||
## Stratégie de migration
|
||||
|
||||
1. **v3.x** : Feature derrière `TASK_X_SCOPED_VARIABLES=1`
|
||||
2. **v4.0** : Activer par défaut (breaking change documenté)
|
||||
3. **Migration** : Mettre à jour les templates `{{.VAR}}` → `{{.namespace.VAR}}`
|
||||
|
||||
---
|
||||
|
||||
## Risques et considérations
|
||||
|
||||
- **Breaking change** : Les Taskfiles existants utilisant des variables d'includes sans préfixe casseront
|
||||
- **Performance** : Négligeable (ajout d'une map)
|
||||
- **Complexité** : Modérée, mais bien isolée dans quelques fichiers
|
||||
248
PLAN_SCOPED_VARIABLES_FINAL.md
Normal file
248
PLAN_SCOPED_VARIABLES_FINAL.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Plan : Scoped Includes (Lazy DAG)
|
||||
|
||||
## Objectif
|
||||
|
||||
Scoper les variables des Taskfiles inclus via **lazy resolution** sur le DAG, avec **isolation stricte**.
|
||||
|
||||
## Experiment Flag
|
||||
|
||||
```bash
|
||||
TASK_X_SCOPED_INCLUDES=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modèle de scopes
|
||||
|
||||
### Priorité (croissante)
|
||||
|
||||
```
|
||||
1. Environment ← Shell + CLI (task FOO=bar)
|
||||
2. Root vars ← Taskfile racine
|
||||
3. Include vars ← Chaîne d'héritage (parent → enfant)
|
||||
4. Task vars ← Plus haut
|
||||
```
|
||||
|
||||
### Visibilité (isolation stricte)
|
||||
|
||||
| Depuis | Voit | Ne voit PAS |
|
||||
|--------|------|-------------|
|
||||
| Root | Ses vars | Vars des includes |
|
||||
| Include | Ses vars + héritage parent | Vars des siblings |
|
||||
| Task | Toute la chaîne d'héritage | - |
|
||||
|
||||
### Partage de variables
|
||||
|
||||
Pour partager des vars entre plusieurs Taskfiles, utiliser `flatten: true` :
|
||||
|
||||
```yaml
|
||||
includes:
|
||||
common:
|
||||
taskfile: ../common/Taskfile.yml
|
||||
flatten: true # Merge global, pas de scoping
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Considérations spéciales
|
||||
|
||||
### Variables dynamiques (`sh:`)
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
VERSION:
|
||||
sh: git describe --tags
|
||||
```
|
||||
|
||||
- Exécutées dans le `Dir` de leur Taskfile d'origine
|
||||
- Le champ `Var.Dir` stocke le répertoire d'exécution
|
||||
- Cache des résultats pour éviter les re-exécutions
|
||||
|
||||
### Defer
|
||||
|
||||
- Utilise le même cache de variables résolu
|
||||
- Pas d'impact sur l'implémentation
|
||||
|
||||
### Ordre de résolution
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
A: "hello"
|
||||
B: "{{.A}} world"
|
||||
C:
|
||||
sh: echo "{{.B}}"
|
||||
```
|
||||
|
||||
- Variables résolues dans l'ordre de déclaration
|
||||
- Chaîne d'héritage résolue AVANT les vars locales
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Avant (merge global)
|
||||
|
||||
```
|
||||
Reader.Read() → TaskfileGraph
|
||||
↓
|
||||
TaskfileGraph.Merge() → Taskfile unique
|
||||
- Vars mergées globalement (last-one-wins)
|
||||
↓
|
||||
Compiler.TaskfileVars = Toutes les vars mergées
|
||||
```
|
||||
|
||||
### Après (lazy DAG)
|
||||
|
||||
```
|
||||
Reader.Read() → TaskfileGraph
|
||||
↓
|
||||
TaskfileGraph.Merge() → Taskfile racine
|
||||
- Vars NON mergées (restent dans le DAG)
|
||||
- Tasks mergées (comme avant)
|
||||
↓
|
||||
Executor.Graph = DAG préservé
|
||||
↓
|
||||
Compiler.getVariablesLazy(task) → Traverse le DAG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implémentation
|
||||
|
||||
### Étape 1 : Experiment flag
|
||||
|
||||
**Fichier** : `experiments/experiments.go`
|
||||
|
||||
```go
|
||||
var ScopedIncludes Experiment
|
||||
|
||||
func ParseWithConfig(dir string, config *ast.TaskRC) {
|
||||
// ...
|
||||
ScopedIncludes = New("SCOPED_INCLUDES", config, 1)
|
||||
}
|
||||
```
|
||||
|
||||
### Étape 2 : Stocker le DAG dans l'Executor
|
||||
|
||||
**Fichier** : `executor.go`
|
||||
|
||||
```go
|
||||
type Executor struct {
|
||||
// ...
|
||||
Graph *ast.TaskfileGraph
|
||||
}
|
||||
```
|
||||
|
||||
**Fichier** : `setup.go`
|
||||
|
||||
```go
|
||||
func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
graph, err := reader.Read(ctx, node)
|
||||
e.Graph = graph
|
||||
e.Taskfile, err = graph.Merge()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Étape 3 : Ne pas merger les vars (si experiment ON)
|
||||
|
||||
**Fichier** : `taskfile/ast/taskfile.go`
|
||||
|
||||
```go
|
||||
func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include, experimentEnabled bool) error {
|
||||
if !experimentEnabled || include.Flatten {
|
||||
// Legacy ou flatten : merge global
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
}
|
||||
// Sinon : vars restent dans le DAG
|
||||
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
}
|
||||
```
|
||||
|
||||
### Étape 4 : Helpers pour le DAG
|
||||
|
||||
**Fichier** : `taskfile/ast/graph.go`
|
||||
|
||||
```go
|
||||
func (tfg *TaskfileGraph) Root() (*TaskfileVertex, error)
|
||||
|
||||
func (tfg *TaskfileGraph) GetVertexByNamespace(namespace string) (*TaskfileVertex, error)
|
||||
```
|
||||
|
||||
### Étape 5 : Résolution lazy dans le Compiler
|
||||
|
||||
**Fichier** : `compiler.go`
|
||||
|
||||
```go
|
||||
type Compiler struct {
|
||||
// ...
|
||||
Graph *ast.TaskfileGraph
|
||||
varsCache map[string]*ast.Vars // Cache par namespace
|
||||
}
|
||||
|
||||
func (c *Compiler) getVariables(t *ast.Task, call *Call, eval bool) (*ast.Vars, error) {
|
||||
if experiments.ScopedIncludes.Enabled() {
|
||||
return c.getVariablesLazy(t, call, eval)
|
||||
}
|
||||
// Legacy...
|
||||
}
|
||||
|
||||
func (c *Compiler) getVariablesLazy(t *ast.Task, call *Call, eval bool) (*ast.Vars, error) {
|
||||
result := env.GetEnviron()
|
||||
|
||||
// 1. Special vars
|
||||
// 2. Root vars (depuis DAG.Root())
|
||||
// 3. Include chain vars (traverse DAG selon t.Namespace)
|
||||
// 4. CLI vars (call.Vars)
|
||||
// 5. Task vars (t.Vars)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fichiers à modifier
|
||||
|
||||
| Fichier | Changement |
|
||||
|---------|------------|
|
||||
| `experiments/experiments.go` | Ajouter `ScopedIncludes` |
|
||||
| `executor.go` | Ajouter `Graph` |
|
||||
| `setup.go` | Stocker le DAG |
|
||||
| `taskfile/ast/taskfile.go` | Ne pas merger vars si experiment ON |
|
||||
| `taskfile/ast/graph.go` | Helpers `Root()`, `GetVertexByNamespace()` |
|
||||
| `compiler.go` | `getVariablesLazy()` + cache |
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
### Variables
|
||||
1. Héritage : include voit vars du parent
|
||||
2. Override : include peut override une var du parent
|
||||
3. Isolation : parent ne voit PAS vars de l'include
|
||||
4. Siblings : includes ne se voient pas entre eux
|
||||
5. Chaîne : root → a → b fonctionne
|
||||
6. Flatten : `flatten: true` = merge global
|
||||
7. Legacy : flag OFF = comportement inchangé
|
||||
|
||||
### Variables dynamiques
|
||||
8. `sh:` exécuté dans le Dir de l'include
|
||||
9. Var dynamique référençant une var héritée
|
||||
10. Cache des résultats
|
||||
|
||||
### Defer
|
||||
11. Defer a accès aux vars scopées
|
||||
|
||||
---
|
||||
|
||||
## Ordre des commits
|
||||
|
||||
1. `feat(experiments): add SCOPED_INCLUDES experiment`
|
||||
2. `feat(executor): store TaskfileGraph for lazy resolution`
|
||||
3. `feat(graph): add Root() and GetVertexByNamespace() helpers`
|
||||
4. `feat(taskfile): skip var merge when SCOPED_INCLUDES enabled`
|
||||
5. `feat(compiler): implement lazy variable resolution`
|
||||
6. `test: add scoped includes variable tests`
|
||||
315
PLAN_SCOPED_VARIABLES_V2_LAZY_DAG.md
Normal file
315
PLAN_SCOPED_VARIABLES_V2_LAZY_DAG.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# Plan V2 : Scoped Variables avec Lazy DAG
|
||||
|
||||
## Objectif
|
||||
|
||||
Implémenter le scoping des variables pour les Taskfiles inclus avec **lazy resolution via DAG** et **accès cross-namespace**.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- **Architecture** : Lazy resolution via DAG (pas de merge global des variables)
|
||||
- **Accès cross-namespace** : `{{.namespace.VAR}}` pour accéder aux variables d'un include
|
||||
- **Isolation** : Un include ne voit pas les vars du parent (sauf passage explicite via `includes.vars:`)
|
||||
- **Requires/Preconditions** : Reporté à une phase ultérieure
|
||||
|
||||
---
|
||||
|
||||
## Comparaison des approches
|
||||
|
||||
| Aspect | Plan V1 (scoped merge) | Plan V2 (lazy DAG) |
|
||||
|--------|------------------------|---------------------|
|
||||
| Merge des vars | Oui, dans map scopée | Non, préservées dans le DAG |
|
||||
| Résolution | Build time | Runtime (lazy) |
|
||||
| Performance | Résout tout | Résout seulement le nécessaire |
|
||||
| Complexité | Modérée | Plus élevée |
|
||||
| Accès cross-namespace | `{{.ns.VAR}}` via map | `{{.ns.VAR}}` via traversée DAG |
|
||||
|
||||
---
|
||||
|
||||
## Modèle de scopes (priorité croissante)
|
||||
|
||||
```
|
||||
1. Environment Scope (plus bas)
|
||||
- Variables d'environnement shell
|
||||
- Variables CLI (task VAR=value)
|
||||
|
||||
2. Entrypoint Scope
|
||||
- Dotenv globaux du Taskfile racine
|
||||
- Vars globales du Taskfile racine
|
||||
|
||||
3. Include Scope(s) (dans l'ordre d'inclusion)
|
||||
- Variables passées via `includes.vars:`
|
||||
- Dotenv de l'include (futur)
|
||||
- Vars globales de l'include
|
||||
|
||||
4. Task Scope (plus haut)
|
||||
- Variables passées via une autre task
|
||||
- Dotenv au niveau task
|
||||
- Vars au niveau task
|
||||
```
|
||||
|
||||
**Règle** : Chaque scope hérite du précédent et peut override.
|
||||
|
||||
---
|
||||
|
||||
## Visibilité des variables
|
||||
|
||||
| Depuis... | Accès à... | Mécanisme |
|
||||
|-----------|------------|-----------|
|
||||
| Parent | Ses propres vars | `{{.VAR}}` |
|
||||
| Parent | Vars d'un include | `{{.namespace.VAR}}` (traversée DAG) |
|
||||
| Include | Ses propres vars | `{{.VAR}}` |
|
||||
| Include | Vars du parent | Non (via `includes.vars:` si besoin) |
|
||||
| Include | Vars d'un sibling | Non (via parent comme passeur) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture technique
|
||||
|
||||
### Avant (merge global)
|
||||
|
||||
```
|
||||
Reader.Read() → TaskfileGraph
|
||||
↓
|
||||
TaskfileGraph.Merge() → Taskfile unique
|
||||
- Vars mergées globalement (last-one-wins)
|
||||
- Tasks mergées avec namespace
|
||||
↓
|
||||
Executor.Taskfile = Taskfile unique
|
||||
↓
|
||||
Compiler.TaskfileVars = Taskfile.Vars (toutes mergées)
|
||||
```
|
||||
|
||||
### Après (lazy DAG)
|
||||
|
||||
```
|
||||
Reader.Read() → TaskfileGraph
|
||||
↓
|
||||
TaskfileGraph.Merge() → Taskfile racine
|
||||
- Vars NON mergées (restent dans chaque vertex du DAG)
|
||||
- Tasks mergées avec namespace (comme avant)
|
||||
↓
|
||||
Executor.Taskfile = Taskfile racine
|
||||
Executor.Graph = TaskfileGraph (NOUVEAU)
|
||||
↓
|
||||
Compiler.Graph = TaskfileGraph
|
||||
Compiler.resolveVariables(task) → traverse le DAG lazy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étapes d'implémentation
|
||||
|
||||
### Étape 1 : Ajouter l'experiment flag
|
||||
|
||||
**Fichier** : `experiments/experiments.go`
|
||||
|
||||
```go
|
||||
var ScopedVariables Experiment
|
||||
|
||||
func ParseWithConfig(dir string, config *ast.TaskRC) {
|
||||
ScopedVariables = New("SCOPED_VARIABLES", config, 1)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 2 : Préserver le DAG dans l'Executor
|
||||
|
||||
**Fichier** : `executor.go` + `setup.go`
|
||||
|
||||
```go
|
||||
type Executor struct {
|
||||
// ... existant ...
|
||||
Taskfile *ast.Taskfile
|
||||
Graph *ast.TaskfileGraph // NOUVEAU
|
||||
}
|
||||
|
||||
func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
graph, err := reader.Read(ctx, node)
|
||||
e.Graph = graph // Stocker le DAG
|
||||
e.Taskfile, err = graph.Merge() // Merge pour les tasks
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 3 : Modifier le merge pour ne pas merger les variables
|
||||
|
||||
**Fichier** : `taskfile/ast/taskfile.go`
|
||||
|
||||
```go
|
||||
func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
|
||||
// ... validations existantes ...
|
||||
|
||||
if experiments.ScopedVariables.Enabled() {
|
||||
// NE PAS merger les variables - elles restent dans le DAG
|
||||
// Optionnel : merger seulement les vars passées via include.Vars
|
||||
} else {
|
||||
// Comportement legacy
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
}
|
||||
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 4 : Passer le DAG au Compiler
|
||||
|
||||
**Fichier** : `compiler.go`
|
||||
|
||||
```go
|
||||
type Compiler struct {
|
||||
// ... existant ...
|
||||
Graph *ast.TaskfileGraph // NOUVEAU
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 5 : Résolution lazy des variables
|
||||
|
||||
**Fichier** : `compiler.go`
|
||||
|
||||
Nouvelle méthode pour résoudre les variables en traversant le DAG :
|
||||
|
||||
```go
|
||||
func (c *Compiler) resolveVariablesFromGraph(t *ast.Task) (*ast.Vars, error) {
|
||||
result := env.GetEnviron()
|
||||
|
||||
// 1. Variables spéciales
|
||||
specialVars, _ := c.getSpecialVars(t, call)
|
||||
|
||||
// 2. Variables du Taskfile racine (entrypoint scope)
|
||||
rootVars := c.getRootTaskfileVars()
|
||||
|
||||
// 3. Variables de la chaîne d'includes jusqu'à cette task (include scope)
|
||||
// Traverser le DAG en suivant le namespace de la task
|
||||
includeChainVars := c.getIncludeChainVars(t.Namespace)
|
||||
|
||||
// 4. Variables de la task elle-même (task scope)
|
||||
taskVars := t.Vars
|
||||
|
||||
// Merger dans l'ordre de priorité
|
||||
// ...
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Compiler) getIncludeChainVars(namespace string) *ast.Vars {
|
||||
// Trouver le vertex de l'include dans le DAG
|
||||
// Récupérer ses variables
|
||||
// Récursivement remonter si includes imbriqués
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 6 : Supporter `{{.namespace.VAR}}` dans le templater
|
||||
|
||||
**Fichier** : `internal/templater/templater.go`
|
||||
|
||||
Deux approches possibles :
|
||||
|
||||
**A) Pré-populer une map avec les namespaces accessibles :**
|
||||
```go
|
||||
func (c *Compiler) buildTemplateVars(t *ast.Task) map[string]any {
|
||||
m := make(map[string]any)
|
||||
|
||||
// Variables locales
|
||||
for k, v := range localVars {
|
||||
m[k] = v
|
||||
}
|
||||
|
||||
// Namespaces accessibles (depuis la perspective de cette task)
|
||||
for _, include := range c.getAccessibleIncludes(t) {
|
||||
m[include.Namespace] = c.getIncludeVarsAsMap(include)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
```
|
||||
|
||||
**B) Custom template function :**
|
||||
```go
|
||||
// {{ns "db" "HOST"}} au lieu de {{.db.HOST}}
|
||||
funcMap := template.FuncMap{
|
||||
"ns": func(namespace, varName string) any {
|
||||
return c.resolveNamespacedVar(namespace, varName)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Recommandation** : Approche A pour garder la syntaxe `{{.db.HOST}}`
|
||||
|
||||
---
|
||||
|
||||
### Étape 7 : Gérer les includes imbriqués
|
||||
|
||||
Pour `root → a → b`, les variables doivent être accessibles comme :
|
||||
- `{{.a.VAR}}` depuis root
|
||||
- `{{.a.b.VAR}}` depuis root (via a)
|
||||
- `{{.b.VAR}}` depuis a
|
||||
- `{{.VAR}}` depuis b (ses propres vars)
|
||||
|
||||
Le namespace complet est déjà tracké dans `Task.Namespace` (e.g., `"a:b:taskname"`).
|
||||
|
||||
---
|
||||
|
||||
### Étape 8 : Tests
|
||||
|
||||
**Nouveau répertoire** : `testdata/includes/scoped_vars/`
|
||||
|
||||
Scénarios :
|
||||
1. Accès cross-namespace : `{{.db.HOST}}` depuis root
|
||||
2. Accès local : `{{.HOST}}` depuis l'include
|
||||
3. Isolation : include ne voit pas les vars du parent
|
||||
4. Override via `includes.vars:`
|
||||
5. Pas de conflits entre includes
|
||||
6. `flatten: true` conserve le comportement legacy
|
||||
7. Includes imbriqués : `{{.a.b.VAR}}`
|
||||
8. Variables dynamiques (`sh:`) dans un include
|
||||
|
||||
---
|
||||
|
||||
## Fichiers à modifier
|
||||
|
||||
| Fichier | Changement |
|
||||
|---------|------------|
|
||||
| `experiments/experiments.go` | Ajouter `ScopedVariables` |
|
||||
| `executor.go` | Ajouter champ `Graph` |
|
||||
| `setup.go` | Stocker le DAG dans l'Executor |
|
||||
| `taskfile/ast/taskfile.go` | Conditionner le merge des vars |
|
||||
| `compiler.go` | Traversée lazy du DAG, accès au Graph |
|
||||
| `internal/templater/templater.go` | (optionnel) Support `{{.ns.VAR}}` |
|
||||
|
||||
---
|
||||
|
||||
## Stratégie de migration
|
||||
|
||||
1. **v3.x** : Feature derrière `TASK_X_SCOPED_VARIABLES=1`
|
||||
2. **v4.0** : Activer par défaut (breaking change documenté)
|
||||
3. **Migration** :
|
||||
- Les vars d'includes ne sont plus accessibles directement → utiliser `{{.namespace.VAR}}`
|
||||
- Les vars du parent ne sont plus visibles dans l'include → passer via `includes.vars:`
|
||||
|
||||
---
|
||||
|
||||
## Avantages de cette approche
|
||||
|
||||
1. **Performance** : Résolution lazy, on ne calcule que ce qui est nécessaire
|
||||
2. **Clarté** : Chaque scope est explicite
|
||||
3. **Pas de conflits** : Les variables ne se marchent plus dessus
|
||||
4. **Réutilisabilité** : Un include peut être utilisé dans différents contextes
|
||||
5. **Utilise l'existant** : Le DAG existe déjà, on l'exploite mieux
|
||||
|
||||
---
|
||||
|
||||
## Risques et considérations
|
||||
|
||||
- **Breaking change** : Important - comportement fondamentalement différent
|
||||
- **Complexité** : La traversée du DAG est plus complexe que le merge
|
||||
- **Debug** : Plus difficile de comprendre d'où vient une variable (ajouter du logging)
|
||||
208
PLAN_SCOPED_VARIABLES_V3_LAZY_SIMPLE.md
Normal file
208
PLAN_SCOPED_VARIABLES_V3_LAZY_SIMPLE.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Plan V3 : Lazy DAG avec Isolation Stricte
|
||||
|
||||
## Objectif
|
||||
|
||||
Implémenter le scoping des variables via **lazy resolution** et **isolation stricte** (pas de `{{.namespace.VAR}}`).
|
||||
|
||||
## Décisions
|
||||
|
||||
- **Architecture** : Lazy resolution via DAG
|
||||
- **Isolation stricte** : Pas d'accès cross-namespace
|
||||
- **Héritage** : Include hérite du parent, pas l'inverse
|
||||
- **Experiment** : `TASK_X_SCOPED_VARIABLES=1`
|
||||
|
||||
---
|
||||
|
||||
## Modèle de scopes
|
||||
|
||||
```
|
||||
Environment → Shell + CLI vars
|
||||
↓ hérite
|
||||
Entrypoint → Root Taskfile vars
|
||||
↓ hérite
|
||||
Include(s) → Vars de chaque include (dans l'ordre)
|
||||
↓ hérite
|
||||
Task → Vars de la task
|
||||
```
|
||||
|
||||
Chaque scope hérite du précédent et peut override.
|
||||
|
||||
---
|
||||
|
||||
## Visibilité
|
||||
|
||||
| Depuis | Voit | Ne voit PAS |
|
||||
|--------|------|-------------|
|
||||
| Root | Ses vars | Vars des includes |
|
||||
| Include | Ses vars + parent | Vars des siblings |
|
||||
| Task | Toute la chaîne | - |
|
||||
|
||||
**Communication unidirectionnelle** : parent → enfant via `includes.vars:`
|
||||
|
||||
---
|
||||
|
||||
## Changements requis
|
||||
|
||||
### 1. Experiment flag
|
||||
|
||||
**Fichier** : `experiments/experiments.go`
|
||||
|
||||
```go
|
||||
var ScopedVariables Experiment
|
||||
|
||||
func ParseWithConfig(dir string, config *ast.TaskRC) {
|
||||
ScopedVariables = New("SCOPED_VARIABLES", config, 1)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Préserver le DAG
|
||||
|
||||
**Fichier** : `executor.go`
|
||||
|
||||
```go
|
||||
type Executor struct {
|
||||
Taskfile *ast.Taskfile
|
||||
Graph *ast.TaskfileGraph // NOUVEAU
|
||||
}
|
||||
```
|
||||
|
||||
**Fichier** : `setup.go`
|
||||
|
||||
```go
|
||||
func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
graph, err := reader.Read(ctx, node)
|
||||
e.Graph = graph
|
||||
e.Taskfile, err = graph.Merge()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Ne pas merger les vars
|
||||
|
||||
**Fichier** : `taskfile/ast/taskfile.go`
|
||||
|
||||
```go
|
||||
func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
|
||||
if !experiments.ScopedVariables.Enabled() {
|
||||
// Legacy : merge global
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
}
|
||||
// Les vars restent dans le DAG, pas de merge
|
||||
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Résolution lazy dans le Compiler
|
||||
|
||||
**Fichier** : `compiler.go`
|
||||
|
||||
```go
|
||||
type Compiler struct {
|
||||
Graph *ast.TaskfileGraph // NOUVEAU
|
||||
}
|
||||
|
||||
func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
|
||||
if experiments.ScopedVariables.Enabled() {
|
||||
return c.getVariablesLazy(t, call, evaluateShVars)
|
||||
}
|
||||
// Legacy behavior...
|
||||
}
|
||||
|
||||
func (c *Compiler) getVariablesLazy(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
|
||||
result := env.GetEnviron()
|
||||
|
||||
// 1. Special vars
|
||||
specialVars, _ := c.getSpecialVars(t, call)
|
||||
|
||||
// 2. Root taskfile vars (depuis le DAG)
|
||||
rootVertex := c.Graph.Root()
|
||||
for k, v := range rootVertex.Taskfile.Vars.All() {
|
||||
result.Set(k, v)
|
||||
}
|
||||
|
||||
// 3. Include chain vars (traverser le DAG jusqu'à cette task)
|
||||
if t != nil && t.Namespace != "" {
|
||||
includeVars := c.resolveIncludeChain(t.Namespace)
|
||||
for k, v := range includeVars.All() {
|
||||
result.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Task vars
|
||||
if t != nil {
|
||||
for k, v := range t.Vars.All() {
|
||||
result.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Compiler) resolveIncludeChain(namespace string) *ast.Vars {
|
||||
// namespace = "a:b:taskname" → trouver les vars de a, puis b
|
||||
// Traverser le DAG en suivant les edges
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Helper pour trouver un vertex par namespace
|
||||
|
||||
**Fichier** : `taskfile/ast/graph.go`
|
||||
|
||||
```go
|
||||
func (tfg *TaskfileGraph) GetVertexByNamespace(namespace string) (*TaskfileVertex, error) {
|
||||
// Trouver le vertex correspondant au namespace
|
||||
}
|
||||
|
||||
func (tfg *TaskfileGraph) Root() *TaskfileVertex {
|
||||
// Retourner le vertex racine
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fichiers à modifier
|
||||
|
||||
| Fichier | Changement |
|
||||
|---------|------------|
|
||||
| `experiments/experiments.go` | Ajouter `ScopedVariables` |
|
||||
| `executor.go` | Ajouter `Graph` |
|
||||
| `setup.go` | Stocker le DAG |
|
||||
| `taskfile/ast/taskfile.go` | Conditionner le merge |
|
||||
| `taskfile/ast/graph.go` | Helpers pour accès au DAG |
|
||||
| `compiler.go` | `getVariablesLazy()` |
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
1. **Héritage** : Include voit les vars du parent
|
||||
2. **Override** : Include peut override une var du parent
|
||||
3. **Isolation** : Parent ne voit pas les vars de l'include
|
||||
4. **Siblings** : Un include ne voit pas les vars d'un autre
|
||||
5. **Chaîne** : `root → a → b` - b voit les vars de a et root
|
||||
6. **Legacy** : Sans le flag, comportement inchangé
|
||||
|
||||
---
|
||||
|
||||
## Avantages
|
||||
|
||||
1. **Simple** : Pas de mapping cross-namespace
|
||||
2. **Performance** : Lazy, résout que le nécessaire
|
||||
3. **Clair** : Héritage linéaire, facile à comprendre
|
||||
4. **Safe** : Experiment flag, rollback facile
|
||||
|
||||
---
|
||||
|
||||
## Inconvénient
|
||||
|
||||
Le parent ne peut pas "lire" les vars d'un include. Si nécessaire, il faudrait ajouter `{{.namespace.VAR}}` plus tard (V2).
|
||||
Reference in New Issue
Block a user