diff --git a/.claude/skills/publicar-version/SKILL.md b/.claude/skills/publicar-version/SKILL.md index 8e44ec8..178f5fb 100644 --- a/.claude/skills/publicar-version/SKILL.md +++ b/.claude/skills/publicar-version/SKILL.md @@ -12,6 +12,6 @@ Las etiquetas (`vX.Y.Z`), las releases y mover la etiqueta móvil `v1` las hace 3. **CHANGELOG.md**: mueve lo de «Sin publicar» a `## [X.Y.Z] - AAAA-MM-DD` con Añadido / Cambiado / Corregido en español y actualiza los enlaces de comparación del final (`[Sin publicar]` → `vX.Y.Z...HEAD` y una línea `[X.Y.Z]`). 4. **Docs**: README (solo lo de usuario), `docs/USO.md`, `docs/DESARROLLO.md`, `AGENTS.md` si cambian reglas, `MEMORY.md` (estado, decisiones con fecha, registro de sesiones) y `PLAN.md`. 5. **Cabecera del README** (solo si cambian los sprites): genera la galería (`go run ./cmd/commitling gallery --out out/site`) y rehaz `docs/img/banner.svg` copiando el grupo `` de `svg/{seed,sprout,sapling,shrub}-happy.svg` y `svg/ancient-radiant.svg`, cada uno dentro de `` (la animación va en el grupo interior, nunca en el que lleva `transform`, o la animación CSS lo anula). -6. **Binario precompilado** (activo desde la v1.0.0): el último commit antes de la etiqueta debe incluir `release/checksums.txt` (`GOFLAGS=-p=2 nice -n 19 scripts/build-release.sh out/rel --write`, con la versión de Go de `go.mod`), generado **después** del último cambio de código o de la web (la plantilla va embebida en el binario). El workflow `release.yml` sube los binarios al publicar la release. Si no lo haces, no pasa nada: la Action compila como siempre. Si vuelves a tocar código después, regenéralo o el CI fallará; y tras publicar, el primer PR que cambie código lo borra (`git rm release/checksums.txt`). +6. **Binario precompilado** (activo desde la v1.0.0): el último commit antes de la etiqueta debe incluir `release/checksums.txt` (`GOFLAGS=-p=2 nice -n 19 scripts/build-release.sh out/rel --write`, con la versión de Go de `go.mod`), generado **después** del último cambio de código o de la web (la plantilla va embebida en el binario). El workflow `release.yml` sube los binarios al publicar la release y, desde la 1.2.0, firma una atestación de procedencia de cada uno: compruébala tras «Release» y antes de mover `v1`: `gh release download vX.Y.Z --repo BertMarti/commitling -p 'commitling_*' -D out/att` y, para los 6 binarios, `gh api "repos/BertMarti/commitling/attestations/sha256:$(sha256sum | cut -d' ' -f1)" -q '.attestations | length'` (debe dar 1 o más; funciona con cualquier `gh`, también el 2.46 de la Raspberry). Verificar la firma necesita `gh` 2.49 o posterior: `gh attestation verify --repo BertMarti/commitling --signer-workflow BertMarti/commitling/.github/workflows/release.yml` (no instales otro `gh` en la Raspberry: hazlo desde otra máquina o fíate de la comprobación por API). Si no lo haces, no pasa nada: la Action compila como siempre. Si vuelves a tocar código después, regenéralo o el CI fallará; y tras publicar, el primer PR que cambie código lo borra (`git rm release/checksums.txt`). 7. **Comprueba**: `gofmt -l .`, `go vet ./...`, `GOFLAGS=-p=2 go test ./...`, el subagente `guardian-compatibilidad` y, si tocaste la web, la skill `probar-web`. 8. **Notas para Alberto** en el PR: «tras fusionar: release `vX.Y.Z` con las notas del CHANGELOG y, si es compatible, mover `v1` a ella». Lo que solo sea cierto tras publicar (avisos de «aún no publicado», ejemplos `@vX.Y.Z`) va en otra rama que se fusiona después. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3541e7d..612607d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,12 @@ name: Release # release/checksums.txt committed in that tag: that file is what the Action # trusts. Without it, nothing is uploaded and the Action keeps compiling with # go run. +# +# Each binary also gets a signed build provenance attestation (Sigstore, free +# for public repositories), only so that people can check a binary they +# downloaded by hand: gh attestation verify --repo BertMarti/commitling. +# The Action does not use attestations (they would need a token and gh): it +# still trusts only release/checksums.txt of its own tree. on: release: types: [published] @@ -20,8 +26,13 @@ jobs: binaries: name: binarios runs-on: ubuntu-latest + # contents: upload the binaries to the release; id-token: the OIDC token + # that Sigstore turns into the short-lived signing certificate; + # attestations: store the attestation in this repository. permissions: contents: write + id-token: write + attestations: write env: TAG: ${{ github.event.release.tag_name }} steps: @@ -55,6 +66,14 @@ jobs: if: steps.check.outputs.upload == 'true' run: scripts/build-release.sh "$RUNNER_TEMP/dist" --check + # Only after --check: what gets attested is exactly what + # release/checksums.txt lists (same names and SHA-256). + - name: Atestar la procedencia de los binarios + if: steps.check.outputs.upload == 'true' + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-checksums: ${{ runner.temp }}/dist/checksums.txt + - name: Subir los binarios a la release if: steps.check.outputs.upload == 'true' env: @@ -68,4 +87,5 @@ jobs: echo '```' cat "$RUNNER_TEMP/dist/checksums.txt" echo '```' + echo "Cada binario tiene una atestación de procedencia: \`gh attestation verify --repo $GH_REPO\`." } >> "$GITHUB_STEP_SUMMARY" diff --git a/AGENTS.md b/AGENTS.md index 78bc040..817926c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,11 @@ Una mascota pixel-art **original** que vive en el README de tu perfil de GitHub. - **Cuarta especie `pumpkin`** (`internal/creature/pumpkin.go`, «Calabaza»). La OG coloca dos especies por fila (`stagePos` en `internal/og/og.go`). - Mismo ciclo que la 1.0.0: el último commit del PR lleva `release/checksums.txt`; lo que solo es cierto tras publicar (quitar «la calabaza llega con la 1.1.0», borrar `release/checksums.txt` de `main`) va en la rama `v1.1-tras-publicar`. +## Versión 1.2.0 (02/11/2026) +- **Atestaciones de procedencia**: `release.yml` llama a `actions/attest-build-provenance` (por SHA) con `subject-checksums` sobre el `checksums.txt` recién verificado con `--check`, antes de `gh release upload`; `id-token: write` y `attestations: write` solo en el job `binaries` (`TestReleaseAttestsTheBinaries`). Son para quien verifica a mano (`gh attestation verify --repo BertMarti/commitling`). **La Action no las usa** ni debe usarlas (pedirían `gh` y token en cada perfil): su única fuente de confianza es `release/checksums.txt` de su árbol; el test prohíbe `attest`/`id-token` en `action.yml`. +- **Filtro de la galería** (`#species-pick` en la plantilla, lógica en `page.js`): bloques `.species-block[data-species]` (galería y compacta) ocultos con `hidden`; sin almacenamiento, sin tocar `location.hash`, valor validado contra las propias radios. Test en Node: `node internal/gallery/testdata/page.test.js internal/gallery/page.js`. +- Mismo ciclo que la 1.1.0: el último commit del PR lleva `release/checksums.txt`; lo que solo es cierto tras publicar (avisos «la 1.2.0 aún no está publicada» en README, `docs/USO.md` y la web; ejemplos `@v1.2.0`; borrar `release/checksums.txt` de `main`) va en la rama `v1.2-tras-publicar`. + ## Reglas de contenido - El personaje es **diseño propio**. Prohibido imitar criaturas de videojuegos, anime o marcas. - Las reglas de fase y ánimo están documentadas en el README y son deterministas (mismos datos, mismo SVG). diff --git a/CHANGELOG.md b/CHANGELOG.md index 322290a..dfb9e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,24 @@ El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y e ## [Sin publicar] +## [1.2.0] - 2026-11-02 + +Binarios que se pueden verificar y una galería más fácil de recorrer. **La interfaz de `action.yml` no cambia** (mismas entradas, misma salida `path`, mismos archivos) y todas las tarjetas SVG son byte a byte las de la v1.1.0: con `@v1` no hay que hacer nada. + +### Añadido + +- **Atestaciones de procedencia de los binarios.** Al publicar una release, el workflow «Release» firma con GitHub (Sigstore) una atestación de procedencia para cada uno de los 6 binarios, que dice de qué commit y de qué workflow salió. Quien se baje un binario a mano puede comprobarlo con `gh attestation verify commitling_1.2.0_linux_amd64 --repo BertMarti/commitling` (que lo firmó un workflow de este repositorio) y, para exigir el de «Release», `--signer-workflow BertMarti/commitling/.github/workflows/release.yml` (`gh` 2.49 o posterior). Explicado en el README, en la guía de uso y en la web (sección «Binarios verificables»). La Action no cambia: sigue ejecutando un binario solo si su SHA-256 coincide con el `release/checksums.txt` de su propia etiqueta y, si no, compila con Go. +- **Filtro «Ver en la galería»** en la web: con un clic se ve una sola especie (sus fases, ánimos, accesorios y tarjeta compacta) en vez de las más de 200 tarjetas seguidas; «Todas» vuelve a enseñarlas. Los enlaces «Ir a:» cambian el filtro si la especie estaba oculta. Accesible con teclado y lector de pantalla (anuncia qué se muestra), no guarda nada en el navegador y, sin JavaScript, la galería se ve entera como antes. + ### Cambiado -- Con la v1.1.0 publicada y `v1` apuntando a ella, el README, la guía de uso y el instalador de la web dejan de avisar de que la calabaza necesita una versión aún sin publicar, y los ejemplos de versión exacta pasan a `@v1.1.0`. -- `release/checksums.txt` sale de `main`: describe los binarios de la v1.1.0 ya publicada (va en su etiqueta) y no los de `main`, que ha cambiado. La próxima versión lo vuelve a generar en su commit. +- Con la v1.1.0 publicada y `v1` apuntando a ella, el README, la guía de uso y el instalador de la web dejaron de avisar de que la calabaza necesitaba una versión aún sin publicar, y los ejemplos de versión exacta pasaron a `@v1.1.0`. +- `release/checksums.txt` vuelve con los SHA-256 de los binarios de la 1.2.0 (tras la 1.1.0 había salido de `main`). +- Versión de la CLI: `1.2.0`. + +### Mantenimiento + +- Go 1.27.3 sigue siendo el último parche de Go 1.27 (sin cambios en `go.mod`); govulncheck sin vulnerabilidades; 0 alertas y 0 PRs abiertas de Dependabot. ## [1.1.0] - 2026-10-31 @@ -198,7 +212,8 @@ Primera versión: el MVP. - Las acciones de los workflows suben de versión para evitar Node 20 (`checkout@v7`, `setup-go@v7`, `configure-pages@v6`, `upload-pages-artifact@v5` y `deploy-pages@v5`) (PR #2). -[Sin publicar]: https://github.com/BertMarti/commitling/compare/v1.1.0...HEAD +[Sin publicar]: https://github.com/BertMarti/commitling/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/BertMarti/commitling/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/BertMarti/commitling/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/BertMarti/commitling/compare/v0.7.0...v1.0.0 [0.7.0]: https://github.com/BertMarti/commitling/compare/v0.6.0...v0.7.0 diff --git a/MEMORY.md b/MEMORY.md index 081b1f7..22b81a2 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,7 +1,11 @@ # MEMORY.md · commitling -Última actualización: 2026-10-31 por Claude Code (Opus 5.5, jefe de la fase 6, rama `v1.1-2026-10-31`) +Última actualización: 2026-11-02 por Claude Code (Opus 5.5, jefe de la fase 7, rama `v1.2-2026-11-02`) ## Estado actual +**Tras publicar la v1.2.0 (rama `v1.2-tras-publicar`, se fusiona después de la release y de mover `v1`)**: sin los avisos «la 1.2.0 aún no está publicada» (README, `docs/USO.md`, web), ejemplos `@v1.2.0` y sin `release/checksums.txt` en `main` (va en la etiqueta v1.2.0). `@main` compila siempre con Go. + +**Versión 1.2.0 (fase 7, 02/11/2026, rama `v1.2-2026-11-02`, PR contra `main`)**: v1.1.0 publicada y `v1` apuntando a ella. Novedades: atestaciones de procedencia de los 6 binarios en `release.yml` (solo para verificar a mano con `gh attestation verify`; la Action sigue confiando solo en `release/checksums.txt`) y filtro «Ver en la galería» en la web. Ninguna tarjeta cambia (huellas v0.5.0 y v1.0.0 en verde). `action.yml` sin cambios. Go 1.27.3 sigue siendo el último parche. El último commit lleva `release/checksums.txt` de la 1.2.0. + **Tras publicar la v1.1.0 (rama `v1.1-tras-publicar`, se fusiona después de la release y de mover `v1`)**: sin el aviso «la calabaza llega con la 1.1.0» (README, `docs/USO.md`, instalador de la web), ejemplos `@v1.1.0` y sin `release/checksums.txt` en `main` (va en la etiqueta v1.1.0). `@main` compila siempre con Go. **Versión 1.1.0 (fase 6, 31/10/2026, rama `v1.1-2026-10-31`, PR contra `main`)**: v1.0.0 publicada y `v1` apuntando a ella. Cuarta especie **calabaza** (`pumpkin`; fases Pepita, Plántula, Calabacita, Calabaza y Calabaza gigante) en la Action, la CLI, la galería, los dos generadores y la OG (ahora dos especies por fila). Musgo, hongo y cactus byte a byte iguales (galería de `main` contra la rama: 180 SVG y favicon idénticos; 12 combinaciones de la CLI idénticas; huellas de v0.5.0). De `action.yml` solo cambia la `description` de `species`. El último commit lleva `release/checksums.txt` de la 1.1.0. Avisos «la calabaza llega con la 1.1.0» en README, `docs/USO.md` y el instalador de la web: los quita la rama `v1.1-tras-publicar` (que además borra `release/checksums.txt` de `main`), que se fusiona después de publicar y mover `v1`. Pasos exactos para el orquestador en el informe de la fase 6. @@ -182,7 +186,13 @@ Pulido de v0.4.0 (rama `agent/builder/v0.4-pulido`, un PR contra `main`, tras la - **OG con dos especies por fila** (`stagePos`): con cuatro filas de una especie no cabía sobre el nombre de la web; con una quinta especie habría que añadir una fila. - **`release/checksums.txt` en el último commit del PR** (como en la 1.0.0): antes de publicar, el paso `bin` recibe 404 y compila; tras publicar, usa el binario. La rama `v1.1-tras-publicar` lo borra de `main` junto con los avisos. +### Fase 7 (02/11/2026) +- **Atestaciones con `subject-checksums`** (no `subject-path` con un glob): se atesta exactamente lo que `--check` acaba de comparar con `release/checksums.txt`, con los mismos nombres. Va antes de subir: si la atestación fallara, la release se queda sin binarios (la Action compila con Go) en vez de tener binarios sin atestar. +- **La Action no verifica atestaciones**: necesitaría `gh`, un token con lectura de atestaciones y red a Sigstore en cada perfil; el SHA-256 comprometido en el propio árbol ya es más fuerte para ella (no depende de nada externo). La atestación es una capa para personas. +- **Filtro de la galería en vez de algo en el SVG**: un detalle de temporada en la tarjeta dependería de la fecha (rompe «mismos datos, mismo SVG», haría commits diarios en los perfiles y las huellas). El filtro es solo web, progresivo y sin almacenamiento (origen compartido). + ## Siguiente paso +-8. v1.2.0: fusionar el PR de `v1.2-2026-11-02`; publicar la release `v1.2.0` sobre el SHA del merge; esperar a que «Release» suba los 6 binarios, `checksums.txt` y las atestaciones; `gh attestation verify` de uno; mover `v1`; probar el perfil; PR de `v1.2-tras-publicar`. Ver `PLAN.md`. -7. v1.1.0: fusionar el PR de `v1.1-2026-10-31`; publicar la release `v1.1.0` sobre el SHA del merge; esperar a que «Release» suba los 6 binarios y `checksums.txt`; mover `v1`; probar el perfil; PR de `v1.1-tras-publicar`. Ver `PLAN.md`. -6. v1.0.0: fusionar el PR de `v1-2026-10-26`; publicar la release `v1.0.0` desde ese commit y mover `v1` (órdenes en el README); comprobar que «Release» sube 6 binarios y `checksums.txt`; abrir y fusionar el PR de `v1-tras-publicar`. Ver `PLAN.md`. -5. Fase 3: fusionar el PR de `fase3-2026-10-25`; tras el despliegue, abrir la web con la consola (Chromium) y comprobar 0 violaciones de CSP/Trusted Types al usar la demo, el generador y el selector de tema; después, la release `v0.8.0` (notas en `CHANGELOG.md`) y mover `v1`. Opcional: activar el binario precompilado (`docs/DESARROLLO.md`). @@ -245,3 +255,4 @@ Pulido de v0.4.0 (rama `agent/builder/v0.4-pulido`, un PR contra `main`, tras la - 2026-10-26 Claude Code · Opus 5.5 (jefe) con OpenCode (GPT-5.6 Luna) (fase3-2026-10-25): especie cactus (mapas rehechos por Claude), selector de tema (Luna, revisado y corregido por Claude), Trusted Types, acciones por SHA, binario precompilado preparado (paso `bin`, `build-release.sh`, `release.yml`, job `binario`), OG para tres especies, docs. - 2026-10-26 Claude Code · Opus 5.5 (jefe) con OpenCode (Gemini 3 Flash) (v1-2026-10-26): versión 1.0.0 (CLI, web, docs, CHANGELOG), `release/checksums.txt` de la 1.0.0, jobs `action` y `binario` independientes del estado de la release, docs sin promesas falsas (Marketplace, «`v1` aún no existe»), rama `v1-tras-publicar`; notas de la release redactadas por Gemini y revisadas por Claude. - 2026-10-31 Claude Code · Opus 5.5 (jefe) con OpenCode (GPT-5.6 Luna) (v1.1-2026-10-31): cuarta especie calabaza (mapas de Claude; cableado, tests y docs de Luna, revisados), OG en dos columnas, versión 1.1.0, `release/checksums.txt` de la 1.1.0 y rama `v1.1-tras-publicar`. +- 2026-11-02 Claude Code · Opus 5.5 (jefe) con OpenCode (GPT-5.6 Luna) (v1.2-2026-11-02): atestaciones de procedencia en `release.yml` (Claude) y docs de verificación, filtro «Ver en la galería» y sección «Binarios verificables» de la web (Luna, revisado por Claude), versión 1.2.0, `release/checksums.txt` de la 1.2.0 y rama `v1.2-tras-publicar`. diff --git a/PLAN.md b/PLAN.md index d028844..fe28588 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,6 +1,6 @@ # PLAN.md · commitling -Hoja de ruta priorizada. Fechada el 24/10/2026 y actualizada el 31/10/2026 (versión 1.1.0). Las etiquetas, releases y mover `v1` los hace Alberto (o el orquestador en su nombre). +Hoja de ruta priorizada. Fechada el 24/10/2026 y actualizada el 02/11/2026 (versión 1.2.0). Las etiquetas, releases y mover `v1` los hace Alberto (o el orquestador en su nombre). ## Hecho: versión 1.0.0 (26/10/2026, de v0.7.0 a v1.0.0; la 0.8.0 nunca se publicó) - **Fase 1 (24/10, «Blindada»)**: CSP estricta en la web y sin scripts en línea, CI con job «seguridad» (govulncheck, staticcheck, gitleaks), permisos mínimos, dependabot, `SECURITY.md`, `go 1.27.3` (la Action compilaba con Go 1.27.0, con 11 vulnerabilidades), escritura atómica del SVG, README para personas usuarias y `docs/DESARROLLO.md`. @@ -13,15 +13,19 @@ Hoja de ruta priorizada. Fechada el 24/10/2026 y actualizada el 31/10/2026 (vers - **Cuarta especie: la calabaza** (`species: pumpkin`), de la lista «Después de la 1.0»: Pepita, Plántula, Calabacita, Calabaza y Calabaza gigante, en la Action, la CLI, la galería, los generadores de la web y la imagen OG (dos especies por fila). Musgo, hongo y cactus byte a byte iguales. - Mantenimiento: Go 1.27.3 sigue siendo el último parche (sin cambios en `go.mod`), govulncheck limpio, 0 alertas y 0 PRs de Dependabot. +## Hecho: versión 1.2.0 (02/11/2026, rama `v1.2-2026-11-02`) +- **Atestaciones de procedencia de los binarios** (`actions/attest-build-provenance`, fijada por SHA), de la lista «Después de la 1.0»: `release.yml` atesta los 6 binarios tras `--check` y antes de subirlos; `id-token: write` y `attestations: write` solo en ese job (`TestReleaseAttestsTheBinaries`). Verificación para usuarios (`gh attestation verify --repo BertMarti/commitling`) en README, `docs/USO.md`, `SECURITY.md` y la web («Binarios verificables»). La Action sigue confiando solo en `release/checksums.txt`. +- **Filtro «Ver en la galería»** en la web (una especie o todas), sin almacenamiento y con la galería entera sin JavaScript. +- Mantenimiento: Go 1.27.3 sigue siendo el último parche (sin cambios en `go.mod`), govulncheck limpio, 0 alertas y 0 PRs de Dependabot. Hechos y quitados de «Siguiente» los pasos de publicación de la 1.0.0 y la 1.1.0. + ## Siguiente (por orden) -0. **Orquestador/Alberto (1.1.0)**: fusionar el PR de `v1.1-2026-10-31`, publicar `v1.1.0` sobre el SHA del merge, esperar a que «Release» suba los binarios, mover `v1` y fusionar el PR de `v1.1-tras-publicar` (pasos exactos en el informe de la fase 6). -1. **Orquestador/Alberto**: fusionar el PR de `v1-2026-10-26`, publicar la release `v1.0.0` desde ese commit (título y notas en el informe de la fase 5) y mover `v1` a ella (orden exacta en el README, «Lo que tienes que hacer tú»). Comprobar que el workflow «Release» sube los 6 binarios y `checksums.txt`. -2. ~~**Orquestador**: PR de la rama `v1-tras-publicar`~~ Hecho: avisos de «aún no publicada» fuera, ejemplos `@v1.0.0` y `release/checksums.txt` borrado de `main`. -3. Tras el despliegue: abrir https://bertmarti.github.io/commitling/ con la consola y comprobar 0 violaciones de CSP/Trusted Types (demo, generador con un usuario real, selector de tema). La primera ejecución del perfil de BertMarti tras mover `v1` no debe tener el paso de `setup-go` (usa el binario). -4. Subir la línea `go` de `go.mod` con cada parche de Go (y el mínimo de `TestGoModPinsAPatchedGo`); govulncheck en el CI avisará. +1. **Orquestador/Alberto (1.2.0)**: fusionar el PR de `v1.2-2026-11-02`, publicar `v1.2.0` sobre el SHA del merge, esperar a que «Release» suba los binarios **y sus atestaciones**, comprobar una con `gh attestation verify`, mover `v1` y fusionar el PR de `v1.2-tras-publicar` (pasos exactos en el informe de la fase 7, como en la 1.1.0). +2. Tras el despliegue: abrir https://bertmarti.github.io/commitling/ con la consola y comprobar 0 violaciones de CSP/Trusted Types (filtro de la galería, demo, generador, selector de tema). La primera ejecución del perfil de BertMarti tras mover `v1` no debe tener el paso de `setup-go` (usa el binario). +3. Subir la línea `go` de `go.mod` con cada parche de Go (y el mínimo de `TestGoModPinsAPatchedGo`); govulncheck en el CI avisará. ## Después de la 1.0 - **Alberto**: publicar la Action en el GitHub Marketplace (hay que aceptar el acuerdo de desarrollador en la web al editar la release: casilla «Publish this Action to the GitHub Marketplace»). `action.yml` ya cumple los requisitos (`TestActionMetadataIsMarketplaceReady`). -- ~~Una cuarta especie~~ Hecho en la 1.1.0 (calabaza). - Modo «año en píxeles» con la actividad de todo el año (requiere otra fuente de datos: la API de eventos solo da 90 días; probablemente GraphQL con token). -- Atestaciones de procedencia de los binarios (`actions/attest-build-provenance`) como capa extra para quien quiera verificarlos a mano; la Action seguiría confiando solo en `release/checksums.txt`. +- Una quinta especie: la OG necesitaría una fila más (`stagePos` coloca dos por fila). +- ~~Una cuarta especie~~ Hecho en la 1.1.0 (calabaza). +- ~~Atestaciones de procedencia de los binarios~~ Hecho en la 1.2.0. diff --git a/README.md b/README.md index 9fc33c7..1a3a117 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,21 @@ Añade `species: mushroom` (o `species: cactus` o `species: pumpkin`) o `theme: No. Es un diseño original, dibujado píxel a píxel para este proyecto. +
¿Puedo comprobar que un binario de las releases es auténtico? + +Sí. No te hace falta para usar la Action: antes de ejecutar su binario, la propia Action comprueba su huella SHA-256 con la lista que viaja dentro de ella y, si no coincide, lo descarta y compila el código con Go. + +Si te bajas un binario a mano desde las [releases](https://github.com/BertMarti/commitling/releases) (para usar la línea de órdenes sin instalar Go), a partir de la 1.2.0 cada uno lleva una **atestación de procedencia** firmada por GitHub que dice de qué commit y de qué workflow salió. Compruébala con la [CLI de GitHub](https://cli.github.com/) (2.49 o posterior, con la sesión iniciada: `gh auth login`), cambiando el nombre por el del archivo que te hayas bajado: + +```sh +gh attestation verify commitling_1.2.0_linux_amd64 --repo BertMarti/commitling +``` + +Si dice «Verification succeeded», el archivo no ha cambiado ni un byte desde que lo compiló y firmó un workflow de `BertMarti/commitling`. Para exigir además que fuera el workflow «Release» (el que publica los binarios), añade `--signer-workflow BertMarti/commitling/.github/workflows/release.yml`. Hace falta `gh` 2.49 o posterior. + +> La 1.2.0 aún no está publicada: hasta entonces, las releases no tienen atestación. +
+
¿Qué versión uso? Usa `BertMarti/commitling@v1` para recibir mejoras compatibles. Si quieres fijar una versión concreta, usa una etiqueta exacta (por ejemplo `@v1.1.0`) o el SHA de un commit. Evita `@main`: es la rama de desarrollo. @@ -277,6 +292,19 @@ Esto es solo para Alberto, que publica las versiones: ``` Mueve `v1` solo si la versión es compatible (mismas entradas y salida de la Action). Si el commit de la versión lleva `release/checksums.txt`, el workflow «Release» sube los binarios precompilados ([guía de desarrollo](docs/DESARROLLO.md#binario-precompilado)). + + Antes de mover `v1`, cuando «Release» termine, comprueba que los 6 binarios tienen su atestación (desde la 1.2.0). Con `gh` 2.49 o posterior, la firma completa: + + ```bash + gh release download vX.Y.Z --repo BertMarti/commitling -p 'commitling_*' -D out/att + for f in out/att/commitling_*; do gh attestation verify "$f" --repo BertMarti/commitling --signer-workflow BertMarti/commitling/.github/workflows/release.yml || echo "FALLA: $f"; done + ``` + + Con un `gh` más antiguo (sin `gh attestation`), al menos que existan: cada binario debe dar 1 o más. + + ```bash + for f in out/att/commitling_*; do echo "$f $(gh api "repos/BertMarti/commitling/attestations/sha256:$(sha256sum "$f" | cut -d' ' -f1)" -q '.attestations | length')"; done + ``` 2. **Opcional:** publicar la Action en el GitHub Marketplace: edita la release en GitHub, marca «Publish this Action to the GitHub Marketplace» y acepta el acuerdo (solo la primera vez). ## Privacidad diff --git a/SECURITY.md b/SECURITY.md index f1911ce..73756aa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,6 +18,7 @@ Respondo en cuanto pueda (proyecto personal, sin plazos garantizados). - La **GitHub Action** (`action.yml`): solo lee la API pública de eventos con el token que le pases; no escribe en tu repositorio (el commit lo hace tu workflow) y sus entradas llegan al script por variables de entorno, nunca interpoladas en el código. - El **SVG generado**: todo texto que viene de fuera (usuario, nombres) se escapa como XML; el SVG no lleva scripts ni enlaces externos. +- Los **binarios de las releases**: se compilan de forma reproducible y la Action solo ejecuta uno si su SHA-256 coincide con `release/checksums.txt` de su propia etiqueta (si no, compila con Go). A partir de la 1.2.0 llevan además una atestación de procedencia firmada por GitHub: `gh attestation verify --repo BertMarti/commitling` comprueba que el archivo es el que firmó un workflow de este repositorio, y con `--signer-workflow BertMarti/commitling/.github/workflows/release.yml` exige que fuera el de «Release» (`gh` 2.49 o posterior). - La **web** (GitHub Pages): sin dependencias de terceros, con Content-Security-Policy estricta; Trusted Types (la única política crea la URL de `wasm_exec.js`); el generador solo habla con `api.github.com`, no hay cookies y en `localStorage` solo se guarda la preferencia de tema (`commitling:theme`, `light` o `dark`). ## Buenas prácticas para quien usa la Action diff --git a/cmd/commitling/main.go b/cmd/commitling/main.go index 2af2c98..98536ae 100644 --- a/cmd/commitling/main.go +++ b/cmd/commitling/main.go @@ -28,7 +28,7 @@ import ( ) // version is the CLI version; it can be overridden with -ldflags "-X main.version=...". -var version = "1.1.0" +var version = "1.2.0" const usage = `commitling: una mascota pixel-art que crece con tus commits. diff --git a/cmd/commitling/main_test.go b/cmd/commitling/main_test.go index ee8c80f..05dc161 100644 --- a/cmd/commitling/main_test.go +++ b/cmd/commitling/main_test.go @@ -94,7 +94,7 @@ func TestMissingFixture(t *testing.T) { func TestVersionAndGallery(t *testing.T) { var stdout, stderr bytes.Buffer - if err := run([]string{"version"}, &stdout, &stderr); err != nil || stdout.String() != "commitling 1.1.0\n" { + if err := run([]string{"version"}, &stdout, &stderr); err != nil || stdout.String() != "commitling 1.2.0\n" { t.Fatalf("version: %v %q", err, stdout.String()) } dir := t.TempDir() diff --git a/docs/DESARROLLO.md b/docs/DESARROLLO.md index 5f2000c..fd9e63b 100644 --- a/docs/DESARROLLO.md +++ b/docs/DESARROLLO.md @@ -115,7 +115,7 @@ Sin binario, la Action compila la CLI con `go run` en cada ejecución. Desde la - `scripts/build-release.sh ` compila los 6 binarios (Linux, macOS y Windows; amd64 y arm64) de forma **reproducible** (`CGO_ENABLED=0`, `-trimpath`, `-buildvcs=false`, `-buildid=`, la versión exacta de Go de `go.mod`): el mismo código da los mismos bytes en cualquier máquina, así que el SHA-256 se puede subir **antes** de que exista la release. - `--write` copia los SHA-256 a `release/checksums.txt`; `--check` falla si no coinciden. -- `.github/workflows/release.yml` se dispara al **publicar** una release: comprueba que la etiqueta es `v` + la versión de la CLI, recompila con `--check` y sube los binarios y `checksums.txt` a la release. Sin `release/checksums.txt` no sube nada. +- `.github/workflows/release.yml` se dispara al **publicar** una release: comprueba que la etiqueta es `v` + la versión de la CLI, recompila con `--check`, **atesta la procedencia** de los 6 binarios (`actions/attest-build-provenance` con `subject-checksums`, es decir, exactamente lo que lista `checksums.txt`) y sube los binarios y `checksums.txt` a la release. Sin `release/checksums.txt` no sube ni atesta nada. Solo ese job tiene `id-token: write` (el token OIDC con el que Sigstore emite el certificado de firma, de usar y tirar) y `attestations: write`; `TestReleaseAttestsTheBinaries` lo vigila. La atestación es para quien verifica un binario a mano (`gh attestation verify --repo BertMarti/commitling`, opcionalmente con `--signer-workflow BertMarti/commitling/.github/workflows/release.yml`); **la Action no la usa** (pediría `gh` y un token): sigue confiando solo en `release/checksums.txt` de su árbol. - El CI recompila y compara en cada PR si el archivo existe (un cambio de código sin regenerarlo falla). El job `binario` de `action-test.yml` prueba primero la Action con el `release/checksums.txt` del repositorio y la release real (antes de publicarla compila con Go; después usa el binario; el SVG es el mismo) y luego las tres rutas (binario bueno, manipulado y sin release) contra un servidor local. El job `action` fuerza siempre la ruta de `go run` (`COMMITLING_RELEASE_URL` a un puerto cerrado) para seguir comprobando el Go parcheado de cada llamada. Para cada versión (ver `.claude/skills/publicar-version`): en la rama de la versión, **después del último cambio de código** (la web va embebida en el binario: también cuenta la plantilla), `GOFLAGS=-p=2 nice -n 19 scripts/build-release.sh out/rel --write` y commit de `release/checksums.txt`; se fusiona y se publica la release `vX.Y.Z` desde ese commit. Tras publicarla, el primer PR que cambie código **borra** el archivo (`git rm release/checksums.txt`): describe los binarios de la release ya publicada, no los de `main`, y regenerarlo con la misma versión daría SHA-256 que no coinciden con los subidos. La versión siguiente lo vuelve a generar. diff --git a/docs/USO.md b/docs/USO.md index 68c097a..8e3d0bd 100644 --- a/docs/USO.md +++ b/docs/USO.md @@ -344,6 +344,26 @@ Si el workflow falla con «unable to find version», revisa que la línea `uses: No hace falta buscarla en ningún catálogo ni instalar nada: basta la línea `uses: BertMarti/commitling@v1` del paso 2. +### Comprobar un binario descargado (atestación de procedencia) + +Cada release trae la línea de órdenes ya compilada para Linux, macOS y Windows (`commitling___`). La Action usa esos mismos binarios, pero no se fía de la descarga: antes de ejecutarlo comprueba su huella SHA-256 con la lista que viaja dentro de la propia Action (`release/checksums.txt` de la etiqueta) y, si no coincide, lo descarta y compila el código con Go. Para usar la Action no tienes que hacer nada. + +Si te bajas un binario a mano, a partir de la 1.2.0 puedes comprobar de dónde salió: cada uno lleva una **atestación de procedencia** firmada por GitHub (con [Sigstore](https://www.sigstore.dev/)) que dice de qué commit y de qué workflow se compiló. Con la [CLI de GitHub](https://cli.github.com/) y la sesión iniciada (`gh auth login`): + +```sh +gh attestation verify commitling_1.2.0_linux_amd64 --repo BertMarti/commitling +``` + +Cambia el nombre por el del archivo que te hayas bajado (por ejemplo `commitling_1.2.0_windows_amd64.exe`). Si sale «Verification succeeded», el archivo no ha cambiado ni un byte desde que lo compiló y firmó un workflow de `BertMarti/commitling` (solo con `--repo`, la orden no dice cuál); si sale un error, no lo uses. Para exigir además que lo firmara el workflow «Release», el que publica los binarios: + +```sh +gh attestation verify commitling_1.2.0_linux_amd64 --repo BertMarti/commitling --signer-workflow BertMarti/commitling/.github/workflows/release.yml +``` + +Hace falta `gh` 2.49 o posterior (`gh --version`); las versiones anteriores no tienen `gh attestation`. + +> La 1.2.0 aún no está publicada: hasta entonces, las releases (1.1.0 y anteriores) no tienen atestación. + ## 7. Desde la línea de órdenes y la imagen de vista previa Esto es para quien quiera probar commitling en su ordenador (hace falta Go 1.27, sin dependencias externas); no hace falta para usarlo en tu perfil. Desde una copia del repositorio: diff --git a/internal/gallery/action_test.go b/internal/gallery/action_test.go index 2955006..5221445 100644 --- a/internal/gallery/action_test.go +++ b/internal/gallery/action_test.go @@ -436,3 +436,62 @@ func TestBuildReleaseResetsTheEnvironment(t *testing.T) { t.Error("build-release.sh must export GOFLAGS=-p=2 GOEXPERIMENT= GOFIPS140=off right after set -euo pipefail and before go build") } } + +// The release workflow attests the provenance of the binaries it uploads, so +// that people can check a downloaded binary with gh attestation verify. Only +// the binaries job may mint an OIDC token and write attestations, only after +// --check (what is attested is exactly release/checksums.txt) and before the +// upload. The Action itself keeps trusting only release/checksums.txt. +func TestReleaseAttestsTheBinaries(t *testing.T) { + yml := readRepoFile(t, ".github/workflows/release.yml") + if !regexp.MustCompile(`(?m)^permissions: \{\}$`).MatchString(yml) { + t.Error("release.yml must start from permissions: {}") + } + for _, perm := range []string{"id-token: write", "attestations: write"} { + if n := strings.Count(yml, perm); n != 1 { + t.Errorf("release.yml grants %q %d times, want once (binaries job)", perm, n) + } + job := strings.Index(yml, " binaries:\n") + if i := strings.Index(yml, " "+perm+"\n"); job < 0 || i < job { + t.Errorf("%q must be a permission of the binaries job", perm) + } + } + check := strings.Index(yml, `scripts/build-release.sh "$RUNNER_TEMP/dist" --check`) + attest := strings.Index(yml, "uses: actions/attest-build-provenance@") + upload := strings.Index(yml, "gh release upload ") + if check < 0 || attest < check || upload < attest { + t.Error("release.yml must attest after build-release.sh --check and before gh release upload") + } + if !strings.Contains(yml[attest:upload], "subject-checksums: ${{ runner.temp }}/dist/checksums.txt") || + !strings.Contains(yml[attest:upload], "if: steps.check.outputs.upload == 'true'") { + t.Error("the attestation must cover dist/checksums.txt and run only when binaries are uploaded") + } + // No other workflow may write attestations or mint OIDC tokens, except + // the deploy job of the Pages workflow: actions/deploy-pages needs + // id-token: write (with pages: write) and nothing else there has it. + for _, f := range []string{"ci.yml", "action-test.yml", "deploy.yml"} { + other := readRepoFile(t, ".github/workflows/"+f) + if strings.Contains(other, "attestations") { + t.Errorf("%s mentions attestations: only the binaries job of release.yml may write them", f) + } + want := 0 + if f == "deploy.yml" { + want = 1 + deploy := strings.Index(other, " deploy:\n") + if i := strings.Index(other, " id-token: write\n"); deploy < 0 || i < deploy || + !strings.Contains(other[deploy:], "uses: actions/deploy-pages@") || + !strings.Contains(other[deploy:i], " pages: write\n") { + t.Error("deploy.yml: id-token: write belongs only to the deploy job, next to pages: write") + } + } + if n := strings.Count(other, "id-token"); n != want { + t.Errorf("%s mentions id-token %d times, want %d", f, n, want) + } + } + action := readRepoFile(t, "action.yml") + for _, bad := range []string{"attestation", "attest-build-provenance", "id-token"} { + if strings.Contains(action, bad) { + t.Errorf("action.yml mentions %q: the Action must trust only release/checksums.txt", bad) + } + } +} diff --git a/internal/gallery/gallery.go b/internal/gallery/gallery.go index 971f772..56d5f38 100644 --- a/internal/gallery/gallery.go +++ b/internal/gallery/gallery.go @@ -120,6 +120,7 @@ type speciesSection struct { // badgeSection is the compact badges of one species: one per stage. type badgeSection struct { + ID string Name string Figures []Figure } @@ -238,7 +239,7 @@ func Build(dir, version string) (int, error) { p.Species = append(p.Species, sec) // The compact badge of every stage, happy and with no accessories. - badges := badgeSection{Name: sp.Name()} + badges := badgeSection{ID: sp.Slug(), Name: sp.Name()} for _, st := range creature.Stages { f, err := card("compact-"+prefix+st.Slug(), sp, Sample(st, creature.Happy), render.Compact) if err != nil { diff --git a/internal/gallery/gallery_test.go b/internal/gallery/gallery_test.go index 083ad0b..dfdb9e3 100644 --- a/internal/gallery/gallery_test.go +++ b/internal/gallery/gallery_test.go @@ -188,8 +188,8 @@ func TestPageHeadForSharing(t *testing.T) { func TestCopyButtonsAreAccessible(t *testing.T) { page := buildPage(t) buttons := regexp.MustCompile(``).FindAllString(page, -1) - if len(buttons) != 4 { - t.Fatalf("found %d copy buttons, want 4", len(buttons)) + if len(buttons) != 5 { + t.Fatalf("found %d copy buttons, want 5", len(buttons)) } labels := map[string]bool{} for _, b := range buttons { @@ -210,8 +210,8 @@ func TestCopyButtonsAreAccessible(t *testing.T) { t.Errorf("copy button must be type=button: %s", b) } } - if got := strings.Count(page, `class="copy-msg note" role="status" aria-live="polite"`); got != 4 { - t.Errorf("found %d live regions for copy feedback, want 4", got) + if got := strings.Count(page, `class="copy-msg note" role="status" aria-live="polite"`); got != 5 { + t.Errorf("found %d live regions for copy feedback, want 5", got) } // If the clipboard fails the user gets a message and the text is selected. for _, want := range []string{"No se pudo copiar", "navigator.clipboard", "execCommand", "selectNodeContents"} { diff --git a/internal/gallery/generator_test.go b/internal/gallery/generator_test.go index 0072632..381cb55 100644 --- a/internal/gallery/generator_test.go +++ b/internal/gallery/generator_test.go @@ -77,9 +77,9 @@ func TestGeneratorFormIsAccessible(t *testing.T) { } // One species, one theme and one size in the generator, the same three // in the workflow generator of «Instálalo en tu perfil», and «Sistema» in - // the theme picker of the page. - if got := strings.Count(page, `checked>`); got != 7 { - t.Errorf("%d options checked by default, want 7 (species, theme and size, in each form, and the page theme)", got) + // the theme picker of the page, and «Todas» in the gallery filter. + if got := strings.Count(page, `checked>`); got != 8 { + t.Errorf("%d options checked by default, want 8 (species, theme and size, in each form, the page theme and the gallery filter)", got) } if !regexp.MustCompile(`]* checked>`).MatchString(page) { t.Error("the full card must be the default size") diff --git a/internal/gallery/index.html.tmpl b/internal/gallery/index.html.tmpl index c4448b5..cf97fc0 100644 --- a/internal/gallery/index.html.tmpl +++ b/internal/gallery/index.html.tmpl @@ -39,6 +39,7 @@ Galería Reglas Instalar + Verificar Código