793481bb11
- apps_tbd.md: tabla de decision para WIP en working tree (incluir/flag/stash/issue separado) - feature_flags.md (nuevo): doctrina TBD oficial, patrones Go/TS/Bash/Py, branch-by-abstraction, anti-patrones - INDEX: entrada 24 Refs: trunkbaseddevelopment.com/feature-flags y branch-by-abstraction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
192 lines
6.7 KiB
Markdown
192 lines
6.7 KiB
Markdown
## Feature flags: enviar codigo incompleto a master sin romperlo
|
|
|
|
Doctrina oficial de **trunk-based development**: master siempre desplegable. Cuando una feature no cabe en una sola rama corta, o cuando hay WIP que no esta terminado pero el resto si, **el codigo viaja detras de un flag OFF**. Asi master sigue verde y el codigo a medio terminar no llega a usuarios reales.
|
|
|
|
Refs: [trunkbaseddevelopment.com/feature-flags/](https://trunkbaseddevelopment.com/feature-flags/), [trunkbaseddevelopment.com/branch-by-abstraction/](https://trunkbaseddevelopment.com/branch-by-abstraction/).
|
|
|
|
### Cuando usar feature flag
|
|
|
|
| Situacion | Accion |
|
|
|---|---|
|
|
| Feature multi-issue (`0015a`, `0015b`, `0015c`) que llevan dias | Cada sub-issue mergea con flag OFF. Ultimo sub-issue activa flag. |
|
|
| Refactor grande tipo "Branch by Abstraction" (ej. cambiar driver DB) | Crear abstraccion + impl nueva con flag. Eliminar antigua + flag al final. |
|
|
| Cambio con riesgo en produccion que necesita rollback rapido | Flag para apagar sin redeploy. |
|
|
| Despliegue gradual (un PC primero, luego todos) | Flag por PC/usuario/grupo. |
|
|
| WIP detectado al cerrar otra rama | Envolver el codigo a medias en flag OFF, mergear, terminar despues. |
|
|
|
|
### Cuando NO usar feature flag
|
|
|
|
- Bug fix autocontenido → mergear directo, sin flag.
|
|
- Refactor que cabe en una rama corta → directo.
|
|
- Docs, comments, type signatures → directo.
|
|
- Codigo que no compila o no pasa tests → **NO viaja a master, ni con flag**. Flag protege codigo terminado, no roto.
|
|
|
|
### Flag != WIP
|
|
|
|
- **WIP**: codigo a medias, no compila o no testea. NO va a master.
|
|
- **Flag**: codigo terminado y testeado, pero no expuesto al usuario. SI va a master.
|
|
|
|
Si hay 80% terminado y 20% pendiente: completar al menos un slice vertical funcional (compila, pasa tests, se puede activar end-to-end), mergear con flag OFF, dejar el 20% para otra rama. NO mergear el 20% sin proteger.
|
|
|
|
### Archivo de flags
|
|
|
|
`dev/feature_flags.json` en la raiz del repo (registry o app). Formato canonico:
|
|
|
|
```json
|
|
{
|
|
"flags": {
|
|
"<flag-name>": {
|
|
"enabled": false,
|
|
"issue": "0063",
|
|
"description": "Descripcion 1 linea de la feature",
|
|
"added": "2026-05-08",
|
|
"enabled_at": null
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Cuando se activa: cambiar `enabled: true` y rellenar `enabled_at` con fecha. Cuando la feature ya es estable y no necesita rollback (semanas/meses despues): borrar el flag y todas sus ramas condicionales del codigo. **Los flags caducan**; documentar fecha de revision para evitar que se acumulen.
|
|
|
|
### Patron por stack
|
|
|
|
#### Go (apps/services)
|
|
|
|
Cargar flags al arrancar. Patron simple — hashmap en memoria + helper `Enabled(name)`:
|
|
|
|
```go
|
|
// pkg/flags/flags.go (puro hasta donde se pueda)
|
|
package flags
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
)
|
|
|
|
type Flag struct {
|
|
Enabled bool `json:"enabled"`
|
|
Issue string `json:"issue"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type Flags struct{ Flags map[string]Flag `json:"flags"` }
|
|
|
|
func Parse(b []byte) (Flags, error) {
|
|
var f Flags
|
|
err := json.Unmarshal(b, &f)
|
|
return f, err
|
|
}
|
|
|
|
func (f Flags) Enabled(name string) bool {
|
|
flag, ok := f.Flags[name]
|
|
return ok && flag.Enabled
|
|
}
|
|
```
|
|
|
|
Uso:
|
|
|
|
```go
|
|
if flags.Enabled("kanban-stickers") {
|
|
registerStickerRoutes(router)
|
|
}
|
|
```
|
|
|
|
Para flags en frontend embebido: serializar a `/api/flags` y leer desde el cliente (ver TS).
|
|
|
|
#### TypeScript / React
|
|
|
|
Inyectar en build (Vite) o exponer endpoint `/api/flags`:
|
|
|
|
```ts
|
|
// src/flags.ts
|
|
let cache: Record<string, boolean> | null = null;
|
|
|
|
export async function loadFlags(): Promise<Record<string, boolean>> {
|
|
if (cache) return cache;
|
|
const res = await fetch("/api/flags");
|
|
const data = await res.json();
|
|
cache = Object.fromEntries(Object.entries(data.flags).map(([k, v]: [string, any]) => [k, !!v.enabled]));
|
|
return cache;
|
|
}
|
|
|
|
export function isEnabled(name: string): boolean {
|
|
return !!(cache?.[name]);
|
|
}
|
|
```
|
|
|
|
Render condicional:
|
|
|
|
```tsx
|
|
{isEnabled("kanban-stickers") && <StickerToolbar ... />}
|
|
```
|
|
|
|
Para flags en build-time (constantes del bundle), usar `import.meta.env.VITE_FLAG_X` o un plugin Vite que reemplace simbolos.
|
|
|
|
#### Bash / pipelines
|
|
|
|
Lectura directa con `jq`:
|
|
|
|
```bash
|
|
ENABLED=$(jq -r '.flags["my-feature"].enabled' dev/feature_flags.json)
|
|
if [ "$ENABLED" = "true" ]; then
|
|
run_new_path
|
|
else
|
|
run_legacy_path
|
|
fi
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
import json
|
|
from pathlib import Path
|
|
|
|
def flags() -> dict:
|
|
return json.loads(Path("dev/feature_flags.json").read_text())["flags"]
|
|
|
|
def enabled(name: str) -> bool:
|
|
f = flags().get(name)
|
|
return bool(f and f.get("enabled"))
|
|
|
|
if enabled("nuevo-pipeline"):
|
|
run_new()
|
|
else:
|
|
run_legacy()
|
|
```
|
|
|
|
### Branch by Abstraction (caso especial)
|
|
|
|
Para cambios grandes (ej. swap iBatis → Hibernate, swap libreria, swap protocolo):
|
|
|
|
1. **Abstraer**: crear interfaz que envuelve la implementacion antigua. Master sigue verde con la antigua. Mergear.
|
|
2. **Implementar nueva**: bajo la misma interfaz, detras de flag OFF. Tests para ambas. Mergear.
|
|
3. **Activar**: flip flag a ON en commit pequeño. Si rompe, flip OFF de inmediato.
|
|
4. **Eliminar antigua**: borrar codigo legacy + flag + abstraccion. Mergear.
|
|
|
|
Cada paso es un merge corto, master nunca esta roto, hay rollback en cada punto.
|
|
|
|
### Reglas operativas
|
|
|
|
- **Un flag = un proposito**. Si necesitas dos toggles independientes, usa dos flags.
|
|
- **Flag bool por defecto**. Si necesitas A/B/C, sigue siendo bool por nombre (`my-feature-v2`, `my-feature-v3`).
|
|
- **Tests con flag ON y OFF**. CI corre ambos paths cuando el flag toca codigo critico.
|
|
- **Documenta en el issue**: que flag protege que codigo, cuando se va a activar, cuando se va a borrar.
|
|
- **No anidar flags**. Si una rama esta detras de dos flags, simplifica.
|
|
- **Borra el flag**. Cuando la feature lleva semanas activa sin rollback, eliminar el flag es trabajo real, no opcional.
|
|
|
|
### Anti-patrones
|
|
|
|
| Anti-patron | Por que es malo |
|
|
|---|---|
|
|
| `if (flag) { ... } else { ... }` esparcido por 30 archivos | Imposible de borrar. Usar inyeccion / strategy pattern. |
|
|
| Flag que lleva 6 meses ON sin borrar | Deuda tecnica. Borrar el flag y simplificar. |
|
|
| Flag para WIP que no compila | Master roto. Eso no es flag, es WIP — no debe estar en master. |
|
|
| Flag condicional sobre tipos / esquemas DB | Migrations son irreversibles. No se "apaga" una columna. Usar branch-by-abstraction sobre la lectura/escritura, no sobre el schema. |
|
|
| Flag con nombre del autor o del issue (`lucas-experiment`, `flag-0063`) | Sin contexto al releerlo. Nombrarlo por la feature: `kanban-stickers`. |
|
|
|
|
### Comandos relacionados
|
|
|
|
- `/git-branch` — crea rama desde master.
|
|
- `/git-push` — merge --no-ff + push.
|
|
- Para registrar / activar un flag: editar `dev/feature_flags.json` directamente y commitear con el codigo correspondiente. No hay CLI dedicada todavia.
|