diff --git a/docs/adr/0014-gradas-de-prueba-y-puerta-de-calidad.md b/docs/adr/0014-gradas-de-prueba-y-puerta-de-calidad.md index 85817cce..a9333867 100644 --- a/docs/adr/0014-gradas-de-prueba-y-puerta-de-calidad.md +++ b/docs/adr/0014-gradas-de-prueba-y-puerta-de-calidad.md @@ -492,18 +492,21 @@ Sin lista de ficheros —la etiqueta `ci-full`, más ficheros de los que lista l PR, 300 en la comparación de un push), un push sin commit anterior o un fallo de la API— corren todos los carriles de Linux. Un PR sin lista corre también los de plataforma; un push, no. -**Qué concierne a una plataforma** es lo que solo ella compila o lo que cambia cómo se compila -allí: `Cargo.toml`, `Cargo.lock`, `build.rs`, `tauri.conf.json`, `clippy.toml`, el `justfile`, -`versions.env`, `.github/`, los scripts que corren sus jobs (`bootstrap.sh`, `pinned-version.sh`, -`install-tools.sh`) y las reglas mismas; `packaging/windows/` y el manifiesto de Windows solo a -Windows, y `packaging/macos/` solo a macOS. Los ficheros con un `cfg` de plataforma no se -escriben a mano: «Alcance» los calcula en cada ejecución con `scripts/platform-files.sh` sobre un -*sparse checkout* de los `.rs` y se los pasa a `ci-lanes.sh`. Cuenta un `cfg` de `windows`, -`unix`, `target_os` o `target_family` —también `target_os = "linux"`, porque tocar una rama deja -huérfana la otra— y cuenta **el módulo entero** que un `cfg` de plataforma declara con `mod`: -`identity/adapters/windows_store/cng.rs` no lleva ningún `cfg`, y solo compila en Windows. Un -cambio solo en el puente Java no enciende las plataformas: su `.dll` y su `.dylib` las compilan -la nocturna y la entrega. +**Qué concierne a una plataforma** es el código que nombra esa plataforma y lo que cambia cómo +se compila allí: `Cargo.toml`, `build.rs`, `versions.env`, `platforms.yml` y la acción +`setup-runner`, y los scripts que instalan sus herramientas (`bootstrap.sh`, `pinned-version.sh`, +`install-tools.sh`); `packaging/windows/` y el manifiesto de Windows solo a Windows, y +`packaging/macos/` solo a macOS. Los ficheros con un `cfg` de plataforma no se escriben a mano: +«Alcance» los calcula en cada ejecución con `scripts/platform-files.sh` sobre un *sparse +checkout* de los `.rs` y se los pasa a `ci-lanes.sh`, cada uno con su plataforma. Un `cfg` que +nombra `windows` o `not(unix)` es de Windows; uno que nombra `"macos"`, de macOS, y +`not(target_os = "linux")`, de las dos. Cuenta **el módulo entero** que ese `cfg` declara con +`mod`: `identity/adapters/windows_store/cng.rs` no lleva ningún `cfg`, y solo compila en +Windows. Un `cfg` de `unix`, `target_os = "linux"` o `target_family` no enciende ninguna, ni +`Cargo.lock`, `tauri.conf.json`, `clippy.toml`, el `justfile`, el resto de `.github/` o las +reglas mismas: los compila o los prueba Linux, y su efecto en otra plataforma lo ve la nocturna. +Un cambio solo en el puente Java tampoco enciende las plataformas: su `.dll` y su `.dylib` las +compilan la nocturna y la entrega. **La nocturna es la red.** Una vez al día, si `main` ha cambiado desde la nocturna anterior, corre Windows y macOS enteros sobre `main`. Si sale roja, abre una issue con `needs-triage`, o diff --git a/docs/agents/code-host-ci.md b/docs/agents/code-host-ci.md index 26a7d8a7..8a15d9c6 100644 --- a/docs/agents/code-host-ci.md +++ b/docs/agents/code-host-ci.md @@ -159,10 +159,10 @@ todos. Las guardas de `rfirma-app/src-tauri/tests` leen `rfirma-app/src`, `testdata/` y el `justfile`, así que un PR solo de interfaz sigue pagando `Cadena Rust`. La de citas de ADR también lee `docs/adr`, pero ese directorio no enciende el carril: la guarda corre en local, en el commit y en el push. Windows y macOS van al revés: corren solo si algún -fichero les concierne —los `.rs` con un `cfg` de plataforma y los módulos que -ese `cfg` declara, que `scripts/platform-files.sh` calcula en cada ejecución, -más `Cargo.toml`, `Cargo.lock`, `build.rs`, la configuración de Tauri, su -`packaging/`, el `justfile`, `versions.env` y `.github/`—, y en un push nunca: +fichero les concierne —los `.rs` con un `cfg` que nombra esa plataforma y los +módulos que ese `cfg` declara, que `scripts/platform-files.sh` calcula en cada +ejecución, más `Cargo.toml`, `build.rs`, su `packaging/`, `versions.env`, +`platforms.yml` y `setup-runner`—, y en un push nunca: los cubre la nocturna (ADR-0014). Las etiquetas `ci-full`, `ci-windows` y `ci-macos` fuerzan sus carriles en el siguiente push; el resumen de `Alcance` dice qué corre, qué se omitió y por qué. **Un rojo de Windows o macOS en diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 5c2fedcd..88bcd7a9 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -19,13 +19,13 @@ Los arneses que llaman las recetas del `justfile` (ADR-0013). | `design_sync_selection.py` | Deriva de los títulos y del `parameters.designSync` de las historias la entrada de design-sync, el `titleMap`, los `overrides`, la tabla de catálogo de `conventions.md` (entre marcadores) (`just design-sync-selection`, antes de cada `/design-sync`; el CI no la verifica). Falla nombrando la historia con capa desconocida, título fuera de convención o componente sin mapear. | | `token-per-test.sh` | Ejecuta el comando que recibe con una copia privada del almacén de SoftHSM (directorio temporal con su `SOFTHSM2_CONF`, borrado al salir aunque falle), copiada bajo el cerrojo que toma `testdata/softhsm/certs.sh`. Lo llama la receta `coverage` y, como envoltorio de nextest, `.config/nextest.toml` de `rfirma-app/src-tauri`. | | `ci-lanes.sh` | Dice, a partir de los ficheros de un PR o de un push a `main`, qué carriles del CI tienen que correr, los de Linux y los de Windows y macOS. Lo llama el job `scope` de `ci.yml`, no una receta. | -| `platform-files.sh` | Lista los `.rs` de `rfirma-app/src-tauri` con un `cfg` de plataforma y los módulos que ese `cfg` declara, para `ci-lanes.sh`. Lo llama el job `scope` de `ci.yml`. | +| `platform-files.sh` | Lista, con su plataforma (`windows` o `macos`), los `.rs` de `rfirma-app/src-tauri` con un `cfg` de esa plataforma y los módulos que ese `cfg` declara, para `ci-lanes.sh`. Lo llama el job `scope` de `ci.yml`. | | `main-moved.sh` | Dice, a partir de las ejecuciones de un workflow, si `main` se ha movido desde la anterior. Lo llaman las puertas de `nightly.yml` y `warm-release-cache.yml`. | | `packages-manifest.sh` | Escribe y lee el `paquetes.json` de una entrega: una fila por paquete con plataforma, fichero, formato, si es firmable y su aviso; también genera las notas del borrador de la Release. Lo llama la receta `packages-manifest` y lo leen `release.yml`, `preview.yml`, `packaging/verify-packages.sh` y `packaging/repo/`. | | `preview-comment.sh` | Compone el Markdown del comentario fijo de la preview de una PR a partir de los artefactos, el manifiesto y el contexto de la ejecución; no imprime nada si no hay nada que comentar. Lo llama `preview-comment.yml`. | | `tests/outline_test.sh` | Prueba el esqueleto, los tramos y el índice que produce `outline.sh` sobre los fixtures de `tests/fixtures/`. | | `tests/ci_lanes_test.sh` | Prueba qué carriles enciende `ci-lanes.sh` para cada clase de fichero, con la lista de plataforma, en un push y con etiquetas. | -| `tests/platform_files_test.sh` | Prueba `platform-files.sh` sobre un árbol temporal y, contra el repositorio, que el adaptador CNG cuenta como de Windows. | +| `tests/platform_files_test.sh` | Prueba `platform-files.sh` sobre un árbol temporal y, contra el repositorio, que el adaptador CNG cuenta como de Windows y la barra de título de GTK como de ninguna. | | `tests/main_moved_test.sh` | Prueba `main-moved.sh` con respuestas de la API hechas a mano: primera ejecución, sin cambios, canceladas, otro título y API caída. | | `tests/packages_manifest_test.sh` | Prueba el manifiesto de paquetes (entrega completa, candidata, extensión desconocida, firmable, plataformas). | | `tests/preview_comment_test.sh` | Prueba el comentario de la preview: filas por artefacto, avisos del manifiesto, fallo, salida vacía y marcador en la primera línea. | diff --git a/scripts/ci-lanes.sh b/scripts/ci-lanes.sh index 7c537b00..850be31d 100755 --- a/scripts/ci-lanes.sh +++ b/scripts/ci-lanes.sh @@ -4,7 +4,7 @@ # solo si alguno le concierne. Sin ficheros, corren todos. # # Uso: ci-lanes.sh [--platform-files FICHERO] [--no-platforms] [--force windows|macos]... -# --platform-files la salida de scripts/platform-files.sh: rutas exactas y prefijos con '/' +# --platform-files la salida de scripts/platform-files.sh: `plataforma ruta`, exacta o prefijo con '/' # --no-platforms windows y macos no corren, sean cuales sean los ficheros (push a main) # --force el carril corre, sean cuales sean los ficheros (etiqueta del PR) set -euo pipefail @@ -104,12 +104,14 @@ native_ignores() { return 1 } -only_one_platform_compiles() { - local entry +has_cfg_of() { + local platform="$1" file="$2" entry for entry in "${platform_files[@]}"; do + [ "${entry%% *}" = "$platform" ] || continue + entry="${entry#* }" case "$entry" in - */) [[ "$1" == "$entry"* ]] && return 0 ;; - *) [ "$1" = "$entry" ] && return 0 ;; + */) [[ "$file" == "$entry"* ]] && return 0 ;; + *) [ "$file" = "$entry" ] && return 0 ;; esac done return 1 @@ -117,27 +119,25 @@ only_one_platform_compiles() { every_platform_needs() { case "$1" in - rfirma-app/src-tauri/Cargo.toml | rfirma-app/src-tauri/Cargo.lock | rfirma-app/src-tauri/build.rs) return 0 ;; - rfirma-app/src-tauri/tauri.conf.json | rfirma-app/src-tauri/clippy.toml) return 0 ;; - justfile | versions.env | .github/*) return 0 ;; - scripts/ci-lanes.sh | scripts/platform-files.sh) return 0 ;; + rfirma-app/src-tauri/Cargo.toml | rfirma-app/src-tauri/build.rs | versions.env) return 0 ;; + .github/workflows/platforms.yml | .github/actions/setup-runner/*) return 0 ;; scripts/bootstrap.sh | scripts/pinned-version.sh | scripts/install-tools.sh) return 0 ;; esac - only_one_platform_compiles "$1" + return 1 } windows_needs() { case "$1" in packaging/windows/* | rfirma-app/src-tauri/windows-app-manifest.xml | rfirma-app/src-tauri/vcpkg.json) return 0 ;; esac - every_platform_needs "$1" + every_platform_needs "$1" || has_cfg_of windows "$1" } macos_needs() { case "$1" in packaging/macos/*) return 0 ;; esac - every_platform_needs "$1" + every_platform_needs "$1" || has_cfg_of macos "$1" } lanes=(java web rust native landing) diff --git a/scripts/platform-files.sh b/scripts/platform-files.sh index fd49ac1e..699004e8 100755 --- a/scripts/platform-files.sh +++ b/scripts/platform-files.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Lista los ficheros de rfirma-app/src-tauri con un cfg de plataforma y, con '/' final, los modulos que solo compila una. +# Lista, como `windows ` o `macos `, los ficheros de rfirma-app/src-tauri con un cfg de esa plataforma y, con '/' final, los modulos que ese cfg declara. set -euo pipefail tauri=rfirma-app/src-tauri @@ -13,22 +13,33 @@ find "$tauri" -path "$tauri/target" -prune -o -name '*.rs' -type f -print | sort sub(/\.rs$/, "", file) return file } - FNR == 1 { gated = 0; path = ""; dir = FILENAME; sub(/\/[^\/]*$/, "", dir); modules = crate_dir(FILENAME) } + function platforms_of(line, found, off_linux) { + found = "" + off_linux = line ~ /not[(][[:space:]]*target_os[[:space:]]*=[[:space:]]*"linux"[[:space:]]*[)]/ + if (off_linux || line ~ /(^|[^A-Za-z0-9_"-])windows([^A-Za-z0-9_"-]|$)/ || line ~ /"windows"/ || line ~ /not[(][[:space:]]*unix[[:space:]]*[)]/) found = found " windows" + if (off_linux || line ~ /"macos"/) found = found " macos" + return found + } + function emit(platforms, path, n, list, i) { + n = split(platforms, list, " ") + for (i = 1; i <= n; i++) print list[i], path + } + FNR == 1 { gated = ""; path = ""; dir = FILENAME; sub(/\/[^\/]*$/, "", dir); modules = crate_dir(FILENAME) } { attribute = $0 ~ /^[[:space:]]*#\[/ if ($0 ~ /cfg(!|_attr)?[(](.*[^A-Za-z0-9_"-])?(windows|unix|target_os|target_family)([^A-Za-z0-9_"-]|$)/) { - print FILENAME - if ($0 ~ /^[[:space:]]*#\[cfg[(]/) gated = 1 + emit(platforms_of($0), FILENAME) + if ($0 ~ /^[[:space:]]*#\[cfg[(]/) gated = platforms_of($0) } - if (gated && $0 ~ /^[[:space:]]*#\[path[[:space:]]*=/) { + if (gated != "" && $0 ~ /^[[:space:]]*#\[path[[:space:]]*=/) { path = $0; sub(/^[^"]*"/, "", path); sub(/".*/, "", path) } - if (gated && $0 ~ /(^|[][:space:]])mod[[:space:]]+[A-Za-z0-9_]+[[:space:]]*;/) { + if (gated != "" && $0 ~ /(^|[][:space:]])mod[[:space:]]+[A-Za-z0-9_]+[[:space:]]*;/) { name = $0; sub(/.*(^|[][:space:]])mod[[:space:]]+/, "", name); sub(/[^A-Za-z0-9_].*/, "", name) - if (path != "") print dir "/" path - else { print modules "/" name ".rs"; print modules "/" name "/" } - gated = 0; path = ""; next + if (path != "") emit(gated, dir "/" path) + else { emit(gated, modules "/" name ".rs"); emit(gated, modules "/" name "/") } + gated = ""; path = ""; next } - if (!attribute && $0 !~ /^[[:space:]]*(\/\/.*)?$/) { gated = 0; path = "" } + if (!attribute && $0 !~ /^[[:space:]]*(\/\/.*)?$/) { gated = ""; path = "" } } ' | sort -u diff --git a/scripts/tests/ci_lanes_test.sh b/scripts/tests/ci_lanes_test.sh index 61cdd51c..f15a9344 100755 --- a/scripts/tests/ci_lanes_test.sh +++ b/scripts/tests/ci_lanes_test.sh @@ -7,8 +7,10 @@ work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT printf '%s\n' \ - rfirma-app/src-tauri/src/desktop/adapters/paths.rs \ - rfirma-app/src-tauri/src/identity/adapters/windows_store/ \ + "windows rfirma-app/src-tauri/src/desktop/adapters/paths.rs" \ + "macos rfirma-app/src-tauri/src/desktop/adapters/paths.rs" \ + "windows rfirma-app/src-tauri/src/identity/adapters/windows_store/" \ + "macos rfirma-app/src-tauri/src/identity/adapters/macos_store/" \ >"$work/platform-files" check() { @@ -42,18 +44,19 @@ backend="java=false web=false rust=true native=true landing=false" check "sin ficheros" "$all" "$("$lanes" "$tauri/src/adapters/mod.rs" <<'EOF' #[allow(dead_code)] pub mod store; #[cfg(target_os = "linux")] mod gtk; +#[cfg(not(target_os = "linux"))] +mod native; #[cfg(test)] mod tests; pub mod shared; @@ -39,6 +41,10 @@ EOF cat >"$tauri/src/adapters/store/cng.rs" <<'EOF' pub fn sign() {} EOF +cat >"$tauri/src/adapters/trust.rs" <<'EOF' +#[cfg(target_os = "macos")] +mod keychain; +EOF cat >"$tauri/src/domain/name.rs" <<'EOF' #[cfg(feature = "windows-like")] pub fn feature_is_not_a_platform() {} @@ -56,18 +62,20 @@ cat >"$tauri/target/debug/generated.rs" <<'EOF' mod generated; EOF -expected="rfirma-app/src-tauri/build.rs -rfirma-app/src-tauri/src/adapters/gtk.rs -rfirma-app/src-tauri/src/adapters/gtk/ -rfirma-app/src-tauri/src/adapters/lock.rs -rfirma-app/src-tauri/src/adapters/lock/windows_tests.rs -rfirma-app/src-tauri/src/adapters/mod.rs -rfirma-app/src-tauri/src/adapters/store.rs -rfirma-app/src-tauri/src/adapters/store/ -rfirma-app/src-tauri/src/domain/family.rs -rfirma-app/src-tauri/tests/pin_fd.rs -rfirma-app/src-tauri/tests/support.rs -rfirma-app/src-tauri/tests/support/" +expected="macos rfirma-app/src-tauri/src/adapters/mod.rs +macos rfirma-app/src-tauri/src/adapters/native.rs +macos rfirma-app/src-tauri/src/adapters/native/ +macos rfirma-app/src-tauri/src/adapters/trust.rs +macos rfirma-app/src-tauri/src/adapters/trust/keychain.rs +macos rfirma-app/src-tauri/src/adapters/trust/keychain/ +windows rfirma-app/src-tauri/build.rs +windows rfirma-app/src-tauri/src/adapters/lock.rs +windows rfirma-app/src-tauri/src/adapters/lock/windows_tests.rs +windows rfirma-app/src-tauri/src/adapters/mod.rs +windows rfirma-app/src-tauri/src/adapters/native.rs +windows rfirma-app/src-tauri/src/adapters/native/ +windows rfirma-app/src-tauri/src/adapters/store.rs +windows rfirma-app/src-tauri/src/adapters/store/" actual="$(cd "$work" && "$platform_files")" if [ "$actual" != "$expected" ]; then @@ -78,16 +86,21 @@ fi real="$(cd "$root" && "$platform_files")" for needle in \ - rfirma-app/src-tauri/src/identity/adapters/windows_store/ \ - rfirma-app/src-tauri/src/site/adapters/channel/acceptor.rs; do + "windows rfirma-app/src-tauri/src/identity/adapters/windows_store/" \ + "macos rfirma-app/src-tauri/src/identity/adapters/macos_store/" \ + "windows rfirma-app/src-tauri/src/site/adapters/channel/acceptor.rs"; do grep -qxF "$needle" <<<"$real" || { echo "FALLO (repositorio): falta $needle" >&2 exit 1 } done -if grep -qxF rfirma-app/src-tauri/src/lib.rs <<<"$real"; then - echo "FALLO (repositorio): lib.rs no sabe de plataformas" >&2 - exit 1 -fi +for path in \ + rfirma-app/src-tauri/src/lib.rs \ + rfirma-app/src-tauri/src/desktop/adapters/titlebar/gtk_titlebar.rs; do + if grep -q " $path\$" <<<"$real"; then + echo "FALLO (repositorio): $path no es de Windows ni de macOS" >&2 + exit 1 + fi +done echo "platform_files_test: correcto"