Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .github/scripts/publish-linux.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
#!/usr/bin/env bash
# Publica fiscalapi para linux-x64 con Native AOT dentro de AlmaLinux 8 (glibc 2.28). Un binario AOT de Linux solo corre
# en la glibc con la que se enlazó o en una más nueva (Microsoft Learn, "Native AOT deployment"): enlazarlo aquí lo deja
# correr en RHEL 8 y sus derivados, Debian 10+, Ubuntu 20.04+ y SLES 15, en lugar de exigir la glibc 2.35 de Ubuntu 22.04.
#
# Lo usan el job linux-x64 de release.yml y la verificación local, desde la raíz del repo:
# docker run --rm -v "$PWD:/src" -w /src almalinux:8@sha256:<digest> bash .github/scripts/publish-linux.sh <versión> <salida>
set -euo pipefail

if [ "$#" -ne 2 ]; then
Expand All @@ -14,15 +8,10 @@ fi

version="$1"
output="$2"
# La glibc de AlmaLinux 8. Si el binario pide una más nueva, se rompió la promesa de compatibilidad y el release no sigue.
max_glibc="2.28"

# Prerrequisitos de Native AOT en RHEL 8 (Microsoft Learn): clang y zlib-devel; binutils trae objcopy (StripSymbols) y
# objdump. libicu es del SDK, no del binario (InvariantGlobalization). gnupg2 verifica el script de instalación.
dnf install -y --setopt=install_weak_deps=False clang zlib-devel binutils libicu findutils gnupg2 > /dev/null

# El SDK exacto de global.json, con el script oficial (https://dot.net/v1/dotnet-install.sh), que solo se ejecuta si su
# firma GPG es la de Microsoft. El script baja el SDK por HTTPS de builds.dotnet.microsoft.com.
export DOTNET_ROOT=/opt/dotnet
export PATH="$DOTNET_ROOT:$PATH"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
Expand All @@ -32,7 +21,6 @@ curl -fsSL --retry 3 https://dot.net/v1/dotnet-install.sig -o /tmp/dotnet-instal
bash "$(dirname "${BASH_SOURCE[0]}")/verify-dotnet-install.sh" /tmp/dotnet-install.sh /tmp/dotnet-install.sig
bash /tmp/dotnet-install.sh --jsonfile global.json --install-dir "$DOTNET_ROOT" --no-path

# TreatWarningsAsErrors (Directory.Build.props) convierte cualquier advertencia IL de trimming/AOT en un error.
dotnet publish src/FiscalApi.Cli -c Release -r linux-x64 -p:Version="$version" -o "$output"

binary="$output/fiscalapi"
Expand All @@ -44,7 +32,6 @@ if [ "$newest" != "$max_glibc" ]; then
fi
echo "fiscalapi pide glibc $required_glibc o más nueva."

# Humo en la distro más vieja que se soporta, sin el aviso de versión nueva (el contenedor no hereda CI=true).
actual=$(FISCALAPI_NO_UPDATE_CHECK=1 "$binary" --version)
if [ "$actual" != "$version" ]; then
echo "::error::fiscalapi --version devolvió '$actual' en AlmaLinux 8; se esperaba '$version'." >&2
Expand Down
11 changes: 0 additions & 11 deletions .github/scripts/verify-dotnet-install.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
#!/usr/bin/env bash
# Verifica dotnet-install.sh con su firma GPG antes de ejecutarlo (Microsoft Learn, "dotnet-install scripts reference",
# sección "Signature validation of dotnet-install.sh"). La llave pública va versionada junto a este script
# (dotnet-install.asc, descargada de https://dot.net/v1/dotnet-install.asc el 2026-10-13) y se exige la huella que publica
# Microsoft en esa página:
# 2B93 0AB1 228D 11D5 D7F6 B6AC B9CF 1A51 FC7D 3ACF Microsoft DevUXTeamPrague <devuxteamprague@microsoft.com>
# Si Microsoft rota la llave, la verificación falla y la llave nueva se actualiza a propósito, comprobando su huella.
#
# Uso: verify-dotnet-install.sh <dotnet-install.sh> <dotnet-install.sig>
set -euo pipefail

if [ "$#" -ne 2 ]; then
Expand All @@ -19,14 +11,11 @@ signature="$2"
expected_fingerprint="2B930AB1228D11D5D7F6B6ACB9CF1A51FC7D3ACF"
public_key="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/dotnet-install.asc"

# Un anillo de llaves desechable: solo contiene la llave de Microsoft y no toca el del usuario.
GNUPGHOME=$(mktemp -d)
export GNUPGHOME
trap 'rm -rf "$GNUPGHOME"' EXIT
gpg --batch --quiet --import "$public_key"

# gpg --verify sale con 0 aunque la llave no tenga confianza (lo advierte Microsoft Learn); lo que cuenta es la línea
# VALIDSIG del estado, que solo aparece con una firma buena y trae la huella de la llave que firmó.
if ! status=$(gpg --batch --status-fd 1 --verify "$signature" "$script" 2> /dev/null); then
echo "::error::La firma GPG de dotnet-install.sh no es válida." >&2
exit 1
Expand Down
7 changes: 0 additions & 7 deletions .github/scripts/with-test-keychain.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
#!/usr/bin/env bash
# Corre un comando con un llavero de prueba de macOS como llavero por omisión del usuario: desbloqueado, sin bloqueo por
# tiempo y desechable. Así CredentialStoreTests (SecItemAdd/SecItemCopyMatching/SecItemDelete sobre el llavero por omisión)
# no depende del estado del llavero de sesión del runner. Lo usa release.yml:
# bash .github/scripts/with-test-keychain.sh dotnet test FiscalApi.Cli.slnx --filter "FullyQualifiedName~CredentialStoreTests"
set -euo pipefail
# Compatible con el bash 3.2 de macOS: un arreglo vacío se expande con ${a[@]+"${a[@]}"} para no chocar con set -u.

keychain="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/fiscalapi-cli-test.keychain-db"
password="llavero-de-prueba"
# `security` imprime las rutas entre comillas, una por línea; xargs las quita. Las de un runner no llevan espacios.
original_default=$( (security default-keychain -d user 2> /dev/null || true) | xargs)
read -r -a original_search_list <<< "$( (security list-keychains -d user 2> /dev/null || true) | xargs)"

Expand All @@ -22,7 +16,6 @@ restore() {
trap restore EXIT

security create-keychain -p "$password" "$keychain"
# Sin -t ni -l: el llavero no se bloquea solo a mitad de las pruebas.
security set-keychain-settings "$keychain"
security unlock-keychain -p "$password" "$keychain"
security list-keychains -d user -s "$keychain" ${original_search_list[@]+"${original_search_list[@]}"}
Expand Down
5 changes: 0 additions & 5 deletions .github/scripts/with-test-secret-service.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
#!/usr/bin/env bash
# Corre un comando con un Secret Service de prueba: gnome-keyring desbloqueado con una contraseña desechable, en
# la sesión de D-Bus que abre quien lo llama. Lo usan el CI y la verificación local en Docker:
# dbus-run-session -- bash .github/scripts/with-test-secret-service.sh dotnet test FiscalApi.Cli.slnx
set -euo pipefail

printf '%s' "llavero-de-prueba" | gnome-keyring-daemon --unlock --components=secrets > /dev/null

# gnome-keyring publica org.freedesktop.secrets después de volver: se espera a que el nombre exista en el bus,
# con tope, en lugar de dormir un tiempo fijo que en un runner lento no alcanza.
for _ in $(seq 1 50); do
if dbus-send --session --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus.ListNames | grep -q '"org.freedesktop.secrets"'; then
Expand Down
15 changes: 3 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,27 @@ on:
workflow_dispatch:

permissions:
contents: read # Required for actions/checkout
contents: read

# Un push nuevo al mismo PR cancela la corrida anterior: solo importa el último commit.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
# Runner hospedado por GitHub: el CLI no depende de Docker ni del runner self-hosted de fapi-back.
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
with:
# Sin el token en .git/config: ningún paso posterior lo necesita y así no queda al alcance de otro proceso.
persist-credentials: false

# El SDK sale de global.json, el mismo que se usa en local.
- name: Set up .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
with:
global-json-file: global.json

# Prerrequisitos de Native AOT en Linux (el linker clang y zlib) y un Secret Service de prueba para las
# pruebas del almacén de credenciales (libsecret, gnome-keyring y D-Bus con dbus-send).
- name: Install Native AOT and Secret Service prerequisites
run: |
sudo apt-get update
Expand All @@ -43,16 +37,13 @@ jobs:
- name: Build
run: dotnet build FiscalApi.Cli.slnx -c Release --no-restore

# gnome-keyring en una sesión de D-Bus propia: así corren contra libsecret las pruebas de
# CredentialStoreTests, que sin FISCALAPI_CLI_SECRET_SERVICE_TESTS se omiten en Linux.
- name: Test
env:
FISCALAPI_CLI_SECRET_SERVICE_TESTS: "1"
run: >-
dbus-run-session -- bash .github/scripts/with-test-secret-service.sh
dotnet test FiscalApi.Cli.slnx -c Release --no-build

# TreatWarningsAsErrors convierte cualquier advertencia IL de trimming/AOT en un error que rompe este paso.
- name: Publish Native AOT (linux-x64)
run: dotnet publish src/FiscalApi.Cli -r linux-x64 -c Release -o artifacts/linux-x64

Expand Down
45 changes: 7 additions & 38 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
name: Release

# Compila fiscalapi con Native AOT para cada RID en un runner de su SO (AOT no compila entre sistemas operativos) y deja
# un release EN BORRADOR con los empaques y SHA256SUMS. Publicarlo lo decide una persona (U-03).
on:
push:
tags:
- "v*"
# Prueba del pipeline sin crear un tag: el borrador queda como prerelease si la versión lleva "-".
workflow_dispatch:
inputs:
version:
Expand All @@ -15,10 +12,8 @@ on:
type: string

permissions:
contents: read # Required for actions/checkout; solo el job release escribe.
contents: read

# Las corridas del mismo tag o rama van de una en una: la que llega queda pendiente hasta que termine la que está en curso,
# que nunca se cancela. Si llega otra mientras una espera, GitHub cancela la pendiente y deja la más nueva.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
Expand All @@ -37,8 +32,6 @@ jobs:
tag: ${{ steps.version.outputs.tag }}
prerelease: ${{ steps.version.outputs.prerelease }}
steps:
# La versión sale del tag (v1.2.3 → 1.2.3) o de la entrada del workflow_dispatch, y se valida como semver 2.0.0
# antes de compilar: es -p:Version, el --version del binario y lo que compara el aviso de versión nueva.
- name: Resolve version
id: version
env:
Expand All @@ -51,14 +44,12 @@ jobs:
else
version="$INPUT_VERSION"
fi
# El valor sin validar no se imprime: podría traer texto que el runner interprete como comando (::…::).
identifier='(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)'
semver="^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-${identifier}(\.${identifier})*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$"
if ! [[ "$version" =~ $semver ]]; then
echo "::error::La versión del tag o de la entrada 'version' no es semántica (p. ej. v1.2.3 o 1.2.3-rc.1)."
exit 1
fi
# workflow_dispatch sobre un tag: la versión del binario debe ser la del tag.
if [ "$REF_TYPE" = "tag" ] && [ -n "$INPUT_VERSION" ] && [ "$INPUT_VERSION" != "$version" ]; then
echo "::error::La entrada 'version' no coincide con el tag v$version; déjala igual a $version o corre sobre una rama."
exit 1
Expand All @@ -72,12 +63,10 @@ jobs:
} >> "$GITHUB_OUTPUT"
echo "Versión $version (prerelease: $prerelease)."

# En workflow_dispatch el borrador apunta a este commit con --target, pero GitHub ignora target_commitish si el tag
# ya existe: el release quedaría con binarios de otro commit. Por eso un tag existente detiene la corrida.
- name: Ensure the tag does not exist yet
if: github.ref_type != 'tag'
env:
GH_TOKEN: ${{ github.token }} # contents: read del workflow.
GH_TOKEN: ${{ github.token }}
TAG: v${{ steps.version.outputs.version }}
run: |
if output=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --jq .ref 2>&1); then
Expand All @@ -101,7 +90,6 @@ jobs:
os: windows-latest
binary: fiscalapi.exe
package: fiscalapi-win-x64.zip
# El runner solo aloja Docker: el binario se enlaza en AlmaLinux 8 (glibc 2.28), ver publish-linux.sh.
- rid: linux-x64
os: ubuntu-latest
binary: fiscalapi
Expand All @@ -110,7 +98,6 @@ jobs:
os: macos-latest
binary: fiscalapi
package: fiscalapi-osx-arm64.tar.gz
# Runner Intel propio: el humo de --version corre nativo, sin depender de Rosetta en el runner arm64.
- rid: osx-x64
os: macos-26-intel
binary: fiscalapi
Expand All @@ -125,25 +112,19 @@ jobs:
PACKAGE: ${{ matrix.package }}
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_NOLOGO: "1"
# AlmaLinux 8.10 fijada por digest (docker inspect almalinux:8, 2026-10-13); se actualiza a propósito, no sola.
LINUX_BUILD_IMAGE: almalinux:8@sha256:8b469a3a78515e8a18ea8fc727e6a3679e1d0c5ba6f58d5b30be3d0d9b86cffe
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
with:
# Sin el token en .git/config: ningún paso posterior lo necesita y así no queda al alcance de otro proceso.
persist-credentials: false

# En Linux el SDK se instala dentro del contenedor de compilación.
- name: Set up .NET
if: runner.os != 'Linux'
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
with:
global-json-file: global.json

# Con VS 18 (2026) e ILCompiler 10.0.12, vcvarsall.bat llama a vswhere.exe sin ruta y el link falla (MSB3073) si su
# carpeta no está en PATH. windows-latest trae hoy VS 2022 17.14 con "Desktop development with C++"; agregarla
# no cambia nada ahí y protege la compilación cuando el runner pase a VS 2026.
- name: Add the Visual Studio Installer folder to PATH
if: runner.os == 'Windows'
shell: pwsh
Expand All @@ -153,9 +134,6 @@ jobs:
Write-Host "vswhere.exe antes del cambio: $(if ($vswhere) { $vswhere.Source } else { 'no está en PATH' })"
Add-Content -Path $env:GITHUB_PATH -Value $installer

# Ida y vuelta real en el almacén del SO (CLI-002): Credential Manager en Windows y el llavero en macOS, que el CI de
# los PR (Linux) no cubre. El release no sigue si falla, y se exige que todas hayan corrido y pasado (ninguna omitida).
# En macOS corre con un llavero de prueba desbloqueado como llavero por omisión.
- name: Credential store tests (Credential Manager / Keychain)
if: runner.os != 'Linux'
run: |
Expand All @@ -176,7 +154,6 @@ jobs:
exit 1
fi

# TreatWarningsAsErrors (Directory.Build.props) convierte cualquier advertencia IL de trimming/AOT en un error.
- name: Publish Native AOT (${{ matrix.rid }})
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
Expand All @@ -186,7 +163,6 @@ jobs:
dotnet publish src/FiscalApi.Cli -c Release -r "$RID" -p:Version="$VERSION" -o "$PUBLISH_DIR"
fi

# El binario de cada RID responde en su SO. CI=true (lo define Actions) apaga el aviso de versión nueva.
- name: Smoke test
run: |
actual=$("./$PUBLISH_DIR/$BINARY" --version | tr -d '\r')
Expand All @@ -197,8 +173,6 @@ jobs:
"./$PUBLISH_DIR/$BINARY" --help > /dev/null
echo "$RID: fiscalapi $actual"

# Solo el binario: los símbolos (.pdb, .dbg, .dSYM) se quedan fuera, y cada empaque debe traer exactamente una
# entrada, el binario.
- name: Package (zip)
if: runner.os == 'Windows'
shell: pwsh
Expand All @@ -212,7 +186,6 @@ jobs:
exit 1
}

# COPYFILE_DISABLE: el tar de macOS no agrega los archivos ._* de atributos extendidos.
- name: Package (tar.gz)
if: runner.os != 'Windows'
run: |
Expand All @@ -226,13 +199,12 @@ jobs:
fi

- name: Upload package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: package-${{ matrix.rid }}
path: dist/${{ matrix.package }}
if-no-files-found: error
retention-days: 7
# Ya va comprimido.
compression-level: 0

release:
Expand All @@ -241,20 +213,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # Crear el release borrador y subir sus archivos.
contents: write
env:
VERSION: ${{ needs.version.outputs.version }}
TAG: ${{ needs.version.outputs.tag }}
PRERELEASE: ${{ needs.version.outputs.prerelease }}
steps:
- name: Download packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
pattern: package-*
path: dist
merge-multiple: true

# Exactamente los 4 empaques de D-24 y su SHA256SUMS, verificado antes de subirlo.
- name: Checksums
working-directory: dist
run: |
Expand All @@ -273,8 +244,6 @@ jobs:
sha256sum -c SHA256SUMS
cat SHA256SUMS

# Un tag empujado ya existe (--verify-tag). En workflow_dispatch el tag se crea sobre este commit hasta que alguien
# publique el borrador. --generate-notes arma las notas desde los PR; se editan antes de publicar.
- name: Create draft release
env:
GH_TOKEN: ${{ github.token }}
Expand Down
2 changes: 0 additions & 2 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// markdownlint-cli2 para README.md y docs/: el repo envuelve la prosa a 120 columnas, como el código (.editorconfig de
// fapi-back). Las tablas y los bloques de código no se cortan: una salida real del CLI o un comando se copian tal cual.
{
"config": {
"MD013": {
Expand Down
16 changes: 0 additions & 16 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,13 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Con esto una advertencia IL de trimming/AOT rompe el build y el publish en vez de pasar desapercibida. -->
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!--
Los avisos de NuGetAudit (vulnerabilidad baja, moderada, alta y crítica en un paquete) siguen siendo
avisos: dependen de la base de datos de vulnerabilidades del día y romperían el CI sin ningún cambio
en el repo. Se atienden actualizando el paquete, no apagando el aviso.
-->
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
<!-- El binario no depende de la cultura del equipo; AOT no necesita cargar ICU. -->
<InvariantGlobalization>true</InvariantGlobalization>
<!--
Sin el sha del commit en AssemblyInformationalVersion: la opción de versión del CLI y el
User-Agent muestran la versión semántica tal cual, en local y en el CI.
-->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<!-- Convención del repo: un proyecto de pruebas termina en .Tests. -->
<IsTestProject Condition="$(MSBuildProjectName.EndsWith('.Tests'))">true</IsTestProject>
</PropertyGroup>

<!--
Analizadores de trimming y AOT para todo el código que no es de pruebas. Las pruebas corren sobre
CoreCLR con xUnit, que usa reflexión. PublishAot vive solo en el proyecto del binario.
-->
<PropertyGroup Condition="'$(IsTestProject)' != 'true'">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
Expand Down
Loading
Loading