|
| 1 | +--- |
| 2 | +PLAN: "feat: codejob action subcommand to close the loop on PR merge from CI" |
| 3 | +TAG: v0.5.0 |
| 4 | +--- |
| 5 | + |
| 6 | +# Plan — Cerrar el loop de `codejob` desde el merge (GitHub Action) |
| 7 | + |
| 8 | +## 1. Problema (justificación) |
| 9 | + |
| 10 | +Hoy `codejob` cierra el loop **solo en local**. El flujo típico cuando un agente |
| 11 | +(Jules u otro) termina su tarea es: |
| 12 | + |
| 13 | +1. El agente abre un PR. |
| 14 | +2. Reviso el PR desde la tablet — todo bien. |
| 15 | +3. **Tengo que ir a la PC** y ejecutar `codejob` → descarga el repo, hace fetch y |
| 16 | + posiciona el árbol en la rama del PR (fase `review`). |
| 17 | +4. Ejecuto **otra vez** `codejob 'mensaje'` → fusiona el PR y publica la nueva |
| 18 | + versión (`gopush`: tests, tag, cross-compile, cascade, backup). |
| 19 | + |
| 20 | +El paso 3–4 **obliga a estar físicamente frente al computador** con las |
| 21 | +credenciales locales (keyring de Jules, PAT de GitHub, `.env` con la sesión |
| 22 | +`CODEJOB`). Cuando la revisión ya se hizo desde el móvil y "todo está bien", ese |
| 23 | +viaje a la PC es puro overhead: lo único que falta es *fusionar y publicar*, algo |
| 24 | +que no requiere criterio humano adicional. |
| 25 | + |
| 26 | +**Objetivo:** poder **cerrar el PR fusionándolo desde GitHub (móvil/web)** y que |
| 27 | +la nueva versión se publique automáticamente, sin abrir la PC. El flujo local |
| 28 | +actual se mantiene intacto como alternativa. |
| 29 | + |
| 30 | +## 2. Propuesta |
| 31 | + |
| 32 | +Dos piezas: |
| 33 | + |
| 34 | +### 2.1 Nuevo subcomando: `codejob --init-action` |
| 35 | + |
| 36 | +Genera `.github/workflows/codejob.yml` en el repo **si no existe** (idempotente: |
| 37 | +si ya existe, no lo sobreescribe salvo `--force`). El workflow se llama `codejob`. |
| 38 | + |
| 39 | +- Escribe el YAML embebido (`go:embed`) en `.github/workflows/codejob.yml`. |
| 40 | +- Verifica/registra los secrets que el publish en CI necesite, reutilizando el |
| 41 | + `GitHub.SetSecret` / `ListSecrets` que **ya existen** en `github_secrets.go`. |
| 42 | +- No toca nada más; el usuario revisa, commitea y pushea el workflow una sola vez. |
| 43 | + |
| 44 | +### 2.2 GitHub Action `codejob` (publica al fusionar) |
| 45 | + |
| 46 | +El workflow se activa **solo** cuando se cierra un PR **y** ese cierre es un |
| 47 | +**merge** **y** se trata de la finalización de un `codejob`. La discriminación es |
| 48 | +la clave del diseño (§3). |
| 49 | + |
| 50 | +Al dispararse, corre en CI el equivalente al *close-loop* de `codejob 'mensaje'` |
| 51 | +(publish vía `gopush`): tests → tag → cross-compile/release → limpieza. El |
| 52 | +mensaje y el tag salen del **frontmatter de `docs/PLAN.md`** (`PLAN:` / `TAG:`), |
| 53 | +exactamente la misma fuente de verdad que ya usa el flujo local vía |
| 54 | +`CHECK_PLAN.md`. |
| 55 | + |
| 56 | +## 3. Decisión de diseño clave — cómo distinguir "finalizando un codejob" |
| 57 | + |
| 58 | +El requisito "el action solo debe activarse si es un merge y si estamos |
| 59 | +finalizando un codejob" necesita una señal **fiable y sin intervención local**. |
| 60 | + |
| 61 | +La señal elegida: **la presencia de `docs/PLAN.md` (con frontmatter válido) en la |
| 62 | +rama por defecto tras el merge.** |
| 63 | + |
| 64 | +Por qué funciona y por qué es robusta frente al doble-publish: |
| 65 | + |
| 66 | +| Escenario de cierre | ¿Queda `docs/PLAN.md` en la rama base? | Quién publica | |
| 67 | +|---|---|---| |
| 68 | +| **Desde tablet/web** (merge en GitHub) | **Sí** — el flujo local nunca corrió, nunca renombró `PLAN.md`→`CHECK_PLAN.md` | **La Action** | |
| 69 | +| **Desde la PC** (`codejob 'msg'`) | **No** — el flujo local ya renombró/borró `PLAN.md` y lo publicó | El flujo local | |
| 70 | + |
| 71 | +Esto hace ambos caminos **mutuamente excluyentes por construcción**: la Action |
| 72 | +solo actúa cuando el `PLAN.md` sigue presente, es decir, cuando el loop **no** se |
| 73 | +cerró en local. No hace falta coordinación ni locks. (Recordar: en dispatch, |
| 74 | +`Send()` commitea y pushea `docs/PLAN.md` con su frontmatter, así que el archivo |
| 75 | +está en el historial y viaja a la rama del agente y al merge.) |
| 76 | + |
| 77 | +- **Trigger YAML:** `pull_request: types: [closed]` sobre la rama por defecto. |
| 78 | + No se usa filtro `paths:` porque el PR del agente puede **no** modificar |
| 79 | + `PLAN.md` (si el agente no lo tocó, el diff no lo incluye y `paths` no |
| 80 | + dispararía). El filtrado real se hace en el *guard* del job. |
| 81 | +- **Guard del job:** `if: github.event.pull_request.merged == true` (descarta PRs |
| 82 | + cerrados sin merge) **más** un paso que verifica que `docs/PLAN.md` existe y |
| 83 | + tiene frontmatter válido; si no, el job termina en *no-op* (exit 0). |
| 84 | +- **Anti-re-trigger:** tras publicar, la Action **borra `docs/PLAN.md`** y commitea |
| 85 | + la limpieza. Ese commit es un `push` directo, no un `pull_request`, así que no |
| 86 | + vuelve a disparar el workflow. Y el gate queda cerrado para futuros eventos. |
| 87 | + |
| 88 | +## 4. Diagramas |
| 89 | + |
| 90 | +### 4.1 Flujo actual (el cuello de botella) |
| 91 | + |
| 92 | +```mermaid |
| 93 | +flowchart TD |
| 94 | + A[Escribo docs/PLAN.md] --> B[codejob: dispatch al agente] |
| 95 | + B --> C[Agente trabaja y abre PR] |
| 96 | + C --> D[Reviso el PR desde la tablet - OK] |
| 97 | + D --> E{Cerrar el loop} |
| 98 | + E --> F[IR A LA PC] |
| 99 | + F --> G[codejob: fetch + checkout rama PR<br/>fase review] |
| 100 | + G --> H[codejob 'mensaje':<br/>merge PR + gopush publica] |
| 101 | + H --> I[Nueva versión + tag publicados] |
| 102 | +
|
| 103 | + style F fill:#ffdddd,stroke:#c00,stroke-width:2px |
| 104 | + style E fill:#fff3cd,stroke:#d90 |
| 105 | +``` |
| 106 | + |
| 107 | +### 4.2 Flujo propuesto (cerrar desde el móvil) |
| 108 | + |
| 109 | +```mermaid |
| 110 | +flowchart TD |
| 111 | + subgraph SETUP["Una sola vez por repo"] |
| 112 | + S1[codejob --init-action] --> S2[.github/workflows/codejob.yml<br/>+ secrets] --> S3[commit + push del workflow] |
| 113 | + end |
| 114 | +
|
| 115 | + A[Escribo docs/PLAN.md] --> B[codejob: dispatch al agente] |
| 116 | + B --> C[Agente trabaja y abre PR] |
| 117 | + C --> D[Reviso el PR desde la tablet - OK] |
| 118 | + D --> E[Merge del PR en GitHub<br/>desde el movil/web] |
| 119 | +
|
| 120 | + E --> GA{Action codejob} |
| 121 | + GA -->|PR merged == true?| GA2 |
| 122 | + GA2{docs/PLAN.md presente<br/>con frontmatter?} |
| 123 | + GA2 -->|No: cerrado en local| NOOP[no-op, exit 0] |
| 124 | + GA2 -->|Si: finalizando codejob| P[CI: setup Go<br/>tests + gopush<br/>tag + release] |
| 125 | + P --> CLEAN[Borra docs/PLAN.md + commit limpieza] |
| 126 | + CLEAN --> I[Nueva version + tag publicados] |
| 127 | +
|
| 128 | + ALT[Alternativa: sigo pudiendo<br/>correr codejob 'msg' en la PC] -.-> I |
| 129 | +
|
| 130 | + style E fill:#d4edda,stroke:#28a745,stroke-width:2px |
| 131 | + style GA fill:#cce5ff,stroke:#004085 |
| 132 | +``` |
| 133 | + |
| 134 | +## 5. Alcance de la implementación |
| 135 | + |
| 136 | +### 5.1 Archivos nuevos |
| 137 | + |
| 138 | +- `codejob_action.go` — lógica del subcomando: |
| 139 | + - `InitCodejobAction(force bool) error`: crea `.github/workflows/` si falta, |
| 140 | + escribe el YAML embebido si no existe (o con `--force`), y devuelve mensajes |
| 141 | + claros (creado / ya existe / sobreescrito). |
| 142 | + - Plantilla del workflow embebida con `//go:embed templates/codejob_action.yml`. |
| 143 | +- `templates/codejob_action.yml` — el workflow (ver §5.3). |
| 144 | +- `codejob_action_test.go` (en `test/`) — cobertura (ver §6). |
| 145 | + |
| 146 | +### 5.2 Archivos modificados |
| 147 | + |
| 148 | +- `cli.go` — parsear el flag `--init-action` (y `--force`) en `ParseCodeJobArgs`, |
| 149 | + devolviendo un nuevo booleano `isInitAction`. |
| 150 | +- `cmd/codejob/main.go` — atender `--init-action` antes del flujo normal |
| 151 | + (igual que ya hace con `--reset-gh-token`): llamar a `InitCodejobAction`, |
| 152 | + imprimir resultado y salir. |
| 153 | +- `cmd/codejob/main.go` `showHelp()` — documentar el subcomando. |
| 154 | +- `docs/CODEJOB.md` y `docs/diagrams/CODEJOB_FLOW.md` — documentar el modo CI y |
| 155 | + actualizar la tabla de uso. |
| 156 | + |
| 157 | +### 5.3 Contenido del workflow (`.github/workflows/codejob.yml`) |
| 158 | + |
| 159 | +Diseño técnico (basado en `docs/codejob/RUNNER_BEST_PRACTICES.md` — se usa |
| 160 | +`ubuntu-latest` porque hay compilación Go cross-platform, caso C de ese doc): |
| 161 | + |
| 162 | +```yaml |
| 163 | +name: codejob |
| 164 | +on: |
| 165 | + pull_request: |
| 166 | + types: [closed] |
| 167 | +permissions: |
| 168 | + contents: write # crear tag, release, push de limpieza |
| 169 | +jobs: |
| 170 | + publish: |
| 171 | + # Solo merges reales (descarta PR cerrados sin fusionar) |
| 172 | + if: github.event.pull_request.merged == true |
| 173 | + runs-on: ubuntu-latest |
| 174 | + steps: |
| 175 | + - uses: actions/checkout@v4 |
| 176 | + with: |
| 177 | + ref: ${{ github.event.pull_request.base.ref }} |
| 178 | + fetch-depth: 0 # historial completo para tags/gopush |
| 179 | + - name: Gate — ¿estamos finalizando un codejob? |
| 180 | + id: gate |
| 181 | + run: | |
| 182 | + # PLAN.md presente con frontmatter => cierre desde web (no se cerró en local) |
| 183 | + if [ -f docs/PLAN.md ] && head -1 docs/PLAN.md | grep -q '^---'; then |
| 184 | + echo "run=true" >> "$GITHUB_OUTPUT" |
| 185 | + else |
| 186 | + echo "run=false" >> "$GITHUB_OUTPUT" |
| 187 | + echo "No hay codejob pendiente; no-op." |
| 188 | + fi |
| 189 | + - uses: actions/setup-go@v5 |
| 190 | + if: steps.gate.outputs.run == 'true' |
| 191 | + with: { go-version: 'stable' } |
| 192 | + - name: Publicar (close-loop en CI) |
| 193 | + if: steps.gate.outputs.run == 'true' |
| 194 | + env: |
| 195 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 196 | + run: | |
| 197 | + go install github.com/tinywasm/devflow/cmd/codejob@latest |
| 198 | + codejob --ci-publish # nuevo modo: lee frontmatter, publica, limpia PLAN.md |
| 199 | +``` |
| 200 | +
|
| 201 | +> El detalle exacto de los steps (cache de módulos, versión pinneada de la |
| 202 | +> herramienta en vez de `@latest`, publicación a repo público separado cuando el |
| 203 | +> origin es privado) se afina en implementación. Lo esencial es el par |
| 204 | +> **trigger `closed` + guard `merged` + gate `PLAN.md`**. |
| 205 | + |
| 206 | +### 5.4 Nuevo modo `codejob --ci-publish` |
| 207 | + |
| 208 | +Ruta de ejecución **no interactiva** pensada para el runner, que reutiliza la |
| 209 | +maquinaria existente sin depender del estado local (`.env`/keyring/PAT): |
| 210 | + |
| 211 | +- No hay sesión `CODEJOB` en `.env` (el runner es efímero): se salta el manejo de |
| 212 | + fases; el PR **ya está fusionado** por GitHub. |
| 213 | +- Lee `docs/PLAN.md` con `ReadPlanMeta` → `message` y `tag` (`PLAN:`/`TAG:`). |
| 214 | +- Borra `docs/PLAN.md` (equivale al borrado de `CHECK_PLAN.md` del flujo local). |
| 215 | +- Llama al `Publisher.Publish(message, tag, ...)` completo (deps + tag + release), |
| 216 | + usando `GITHUB_TOKEN` para push/tag/release. |
| 217 | +- **No** despacha planes encadenados en CI (requiere API key de Jules); si tras el |
| 218 | + merge hay un `PLAN.md` nuevo distinto, se deja como *follow-up* opcional (§7). |
| 219 | + |
| 220 | +Esto mantiene una única fuente de verdad para el mensaje/tag (frontmatter) y |
| 221 | +reaprovecha `gopush`/`gorelease` sin duplicar lógica. |
| 222 | + |
| 223 | +## 6. Pruebas (test map) |
| 224 | + |
| 225 | +| Comportamiento | Test | |
| 226 | +|---|---| |
| 227 | +| `InitCodejobAction` crea el workflow si no existe | `TestInitCodejobAction_CreatesWhenAbsent` | |
| 228 | +| Es idempotente (no sobreescribe sin `--force`) | `TestInitCodejobAction_NoOverwrite` | |
| 229 | +| `--force` sí sobreescribe | `TestInitCodejobAction_ForceOverwrites` | |
| 230 | +| El YAML embebido es válido y contiene el guard `merged` + gate `PLAN.md` | `TestCodejobActionTemplate_Contract` | |
| 231 | +| `ParseCodeJobArgs` detecta `--init-action` / `--force` | `TestParseCodeJobArgs_InitAction` | |
| 232 | +| `--ci-publish` con `PLAN.md` válido publica con msg/tag del frontmatter | `TestCIPublish_UsesFrontmatter` | |
| 233 | +| `--ci-publish` sin `PLAN.md` es no-op | `TestCIPublish_NoopWhenNoPlan` | |
| 234 | + |
| 235 | +## 7. Riesgos y decisiones abiertas (para tu aprobación) |
| 236 | + |
| 237 | +1. **Señal de discriminación.** Propongo *presencia de `docs/PLAN.md`* por ser |
| 238 | + cero-fricción. Alternativa más explícita: una **label `codejob`** en el PR |
| 239 | + (más autodocumentada, pero requiere que algo la ponga sin la PC — habría que |
| 240 | + añadir el etiquetado al abrir/detectar el PR). ¿Prefieres archivo o label? |
| 241 | +2. **Repos privados con distribución pública.** `gorelease` ya soporta publicar a |
| 242 | + un repo público derivado cuando el origin es privado; en CI eso exige un **PAT |
| 243 | + con acceso al repo público** como secret (`SetSecret` ya existe). Si el repo es |
| 244 | + público, basta `GITHUB_TOKEN`. ¿Incluyo el registro del secret en |
| 245 | + `--init-action`? |
| 246 | +3. **Planes encadenados en CI.** El re-dispatch automático necesita la API key de |
| 247 | + Jules como secret. Lo dejo **fuera** del alcance inicial (queda solo-local), |
| 248 | + salvo que lo quieras dentro. |
| 249 | +4. **Cascade/backup en CI.** El backup asíncrono y el cascade a módulos |
| 250 | + dependientes asumen entorno local; en CI conviene `--no-cascade` y omitir |
| 251 | + backup. Propongo publicar **solo el módulo** en CI y dejar el cascade al flujo |
| 252 | + local. ¿De acuerdo? |
| 253 | + |
| 254 | +## 8. Resumen |
| 255 | + |
| 256 | +Añadir `codejob --init-action` (scaffolding one-shot del workflow) y un modo |
| 257 | +`codejob --ci-publish` que la Action invoca al fusionar un PR de codejob. El |
| 258 | +disparo es preciso — merge real **y** `docs/PLAN.md` aún presente — lo que separa |
| 259 | +limpiamente el cierre-desde-web (publica la Action) del cierre-en-PC (publica el |
| 260 | +flujo local), sin doble publicación. Resultado: cerrar el PR desde el móvil |
| 261 | +publica la versión, sin tocar la PC. |
| 262 | +</content> |
| 263 | +</invoke> |
0 commit comments