Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
19 changes: 19 additions & 0 deletions .github/scripts/dotnet-install.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: BSN Pgp v1.1.0.0

mQENBGPkMDwBCAC4k+2aKmawupYX+NL91FC8mU0Pe8JYBxDuG6+ZbWLywpWvKHUv
WMMPKV8dQrV0q7MMqdbQthSylWLJkkfUgMhSMJ885+0CXS0Inv5yyMeMA8XvgJ2E
GUgrSs5fabwAdPniVjndLmxIaov7xtCPSZAPP0Mjr8c000aTyQIUNsOYPviDUykS
TMP4oQeBBenTLJsauH/IXAq8wbQyQyXjI6yVV13UmUSTe+uwpM+SN/s7B8GpkxOb
+vZ9DmSexeoUvlaAO2MAAux8pogyQdvMDDkP7aXOkEfc9N87B1dXOSWShLvRg04W
aDUfU+Q6EkQk1yGyZeWvCAMf5nVyxmwan5KTABEBAAG0OU1pY3Jvc29mdCBEZXZV
WFRlYW1QcmFndWUgPGRldnV4dGVhbXByYWd1ZUBtaWNyb3NvZnQuY29tPokBOAQT
AQgAIgUCY+QwPAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQuc8aUfx9
Os+aTggAq9wJY0/hS2+R18RBm3CajFqN9be/uZ0uxLfmu05zgAKm6E2rRlSwX12J
x0tcaJAT+Q+P7vVMeBf+B4yOLwOwe2z/MZaxYkckC6kEnnvzXNEhfKtRhapLClHD
7Lo3iinGNUB8Qa1lBX/t+1oAp50YW1pUeNEk7Ppk2daz3/BuoFwaP8fxwVJ9MEFB
NidXwDy+uGQR9KBCU9mo6XZK3bsFW2vByWr4AQMvixw3m5QCY8m+NXRX08J9LD6t
f0G5bcHEZj+t/RbWHt1iQKv/WB62CVdArSbPFYusd73kYtdLUdwELZILcIoPqV1c
AX0xKovPmQNk8ITCopbbCs3jCIVvmw==
=awWT
-----END PGP PUBLIC KEY BLOCK-----
52 changes: 52 additions & 0 deletions .github/scripts/publish-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/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
echo "Uso: publish-linux.sh <versión> <carpeta de salida>" >&2
exit 2
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
export DOTNET_NOLOGO=1
curl -fsSL --retry 3 https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
curl -fsSL --retry 3 https://dot.net/v1/dotnet-install.sig -o /tmp/dotnet-install.sig
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"
required_glibc=$(objdump -T "$binary" | grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' | sed 's/^GLIBC_//' | sort -uV | tail -n 1)
newest=$(printf '%s\n%s\n' "$required_glibc" "$max_glibc" | sort -V | tail -n 1)
if [ "$newest" != "$max_glibc" ]; then
echo "::error::fiscalapi pide glibc $required_glibc; el release promete correr desde glibc $max_glibc." >&2
exit 1
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
exit 1
fi
38 changes: 38 additions & 0 deletions .github/scripts/verify-dotnet-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/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
echo "Uso: verify-dotnet-install.sh <dotnet-install.sh> <dotnet-install.sig>" >&2
exit 2
fi

script="$1"
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
fi
if ! grep -q "^\[GNUPG:\] VALIDSIG $expected_fingerprint " <<< "$status"; then
echo "::error::dotnet-install.sh no está firmado con la llave $expected_fingerprint." >&2
exit 1
fi
echo "dotnet-install.sh: firma GPG válida de la llave $expected_fingerprint."
31 changes: 31 additions & 0 deletions .github/scripts/with-test-keychain.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/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)"

restore() {
if [ -n "$original_default" ]; then
security default-keychain -d user -s "$original_default" || true
fi
security list-keychains -d user -s ${original_search_list[@]+"${original_search_list[@]}"} || true
security delete-keychain "$keychain" || true
}
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[@]}"}
security default-keychain -d user -s "$keychain"

"$@"
20 changes: 20 additions & 0 deletions .github/scripts/with-test-secret-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/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
exec "$@"
fi
sleep 0.2
done

echo "::error::org.freedesktop.secrets no apareció en la sesión de D-Bus en 10 s." >&2
exit 1
67 changes: 67 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: CI-Pipeline

on:
pull_request:
workflow_dispatch:

permissions:
contents: read # Required for actions/checkout

# 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
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
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
sudo apt-get install -y --no-install-recommends clang zlib1g-dev libsecret-1-0 gnome-keyring dbus

- name: Restore
run: dotnet restore FiscalApi.Cli.slnx

- 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

- name: Smoke test
run: |
expected=$(grep -oPm1 "(?<=<Version>)[^<]+" src/FiscalApi.Cli/FiscalApi.Cli.csproj)
actual=$(./artifacts/linux-x64/fiscalapi --version)
if [ "$actual" != "$expected" ]; then
echo "::error::fiscalapi --version devolvió '$actual'; se esperaba '$expected'."
exit 1
fi
./artifacts/linux-x64/fiscalapi --help
Loading
Loading