diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml
new file mode 100644
index 0000000..a713bfd
--- /dev/null
+++ b/.github/workflows/01-ci.yml
@@ -0,0 +1,123 @@
+# Estado esperado de .github/workflows/01-ci.yml al terminar el laboratorio 04.
+# Cambios respecto al 03: el trabajo se parte en dos jobs unidos con `needs`,
+# y los binarios viajan de un job a otro como artefacto.
+#
+# Ojo con el costo: dos jobs significan dos runners, dos checkouts y dos
+# restores. Partir jobs se justifica cuando ganas paralelismo o cuando
+# necesitas condiciones distintas, no por estética.
+
+name: 01 - CI
+
+on:
+ push:
+ branches:
+ - main
+ - 'lab/**'
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ DOTNET_VERSION: '9.0.x'
+ DOTNET_NOLOGO: 'true'
+ DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ outputs:
+ version-sdk: ${{ steps.sdk.outputs.dotnet-version }}
+ steps:
+ - name: Descargar el código al runner
+ uses: actions/checkout@v7
+
+ - name: Instalar el SDK de .NET
+ id: sdk
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Restaurar dependencias
+ run: dotnet restore
+
+ - name: Compilar en Release
+ run: dotnet build --configuration Release --no-restore
+
+ - name: Publicar los binarios compilados
+ uses: actions/upload-artifact@v7
+ with:
+ name: binarios
+ path: |
+ src/**/bin/Release/
+ tests/**/bin/Release/
+ retention-days: 1
+ if-no-files-found: error
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Descargar el código al runner
+ uses: actions/checkout@v7
+
+ - name: Instalar el SDK de .NET
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Bajar los binarios del job anterior
+ uses: actions/download-artifact@v7
+ with:
+ name: binarios
+
+ # El artefacto trae bin/, pero no obj/. `dotnet test --no-build` necesita
+ # el archivo obj/project.assets.json para resolver rutas de salida, así que
+ # hay que restaurar de nuevo. Es rápido porque no vuelve a compilar.
+ - name: Restaurar dependencias
+ run: dotnet restore
+
+ - name: Ejecutar pruebas
+ run: |
+ dotnet test \
+ --configuration Release \
+ --no-build \
+ --logger "trx;LogFileName=resultados.trx" \
+ --results-directory ./resultados
+
+ - name: Publicar resultados de pruebas
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: resultados-de-pruebas
+ path: ./resultados
+ retention-days: 5
+ if-no-files-found: error
+
+ - name: Escribir el resumen del run
+ if: always()
+ run: |
+ TRX=$(find ./resultados -name '*.trx' | head -1)
+ TOTAL=$(grep -o 'total="[0-9]*"' "$TRX" | head -1 | grep -o '[0-9]*')
+ PASADAS=$(grep -o 'passed="[0-9]*"' "$TRX" | head -1 | grep -o '[0-9]*')
+ FALLIDAS=$(grep -o 'failed="[0-9]*"' "$TRX" | head -1 | grep -o '[0-9]*')
+ {
+ echo "### Resultado de las pruebas"
+ echo ""
+ echo "| Métrica | Valor |"
+ echo "|---------|-------|"
+ echo "| Total | ${TOTAL:-0} |"
+ echo "| Pasadas | ${PASADAS:-0} |"
+ echo "| Fallidas | ${FALLIDAS:-0} |"
+ echo ""
+ echo "SDK usado en build: \`${{ needs.build.outputs.version-sdk }}\`"
+ } >> "$GITHUB_STEP_SUMMARY"
\ No newline at end of file
diff --git a/README.md b/README.md
index e183a4c..949f921 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# ⚙️ Workshop: GitHub Intermedio
+Hola
+
## Pipelines, Políticas y Productividad con .NET 9

diff --git a/scripts/verificar-entorno.sh b/scripts/verificar-entorno.sh
new file mode 100644
index 0000000..ebbf26a
--- /dev/null
+++ b/scripts/verificar-entorno.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# Verificación previa al workshop. Ejecuta esto ANTES de la sesión.
+# Si algo falla aquí, resuélvelo antes y no en los primeros diez minutos
+# del laboratorio.
+
+set -u
+fallas=0
+
+check() {
+ local nombre="$1"; shift
+ if "$@" > /dev/null 2>&1; then
+ printf ' OK %s\n' "$nombre"
+ else
+ printf ' FALLA %s\n' "$nombre"
+ fallas=$((fallas + 1))
+ fi
+}
+
+echo "Verificando herramientas"
+check "git instalado" git --version
+check "dotnet instalado" dotnet --version
+check "gh (GitHub CLI) instalado" gh --version
+check "gh autenticado" gh auth status
+
+echo
+echo "Verificando el proyecto"
+check "restore" dotnet restore
+check "build" dotnet build --configuration Release --no-restore
+check "test" dotnet test --configuration Release --no-build
+
+echo
+if [ "$fallas" -eq 0 ]; then
+ echo "Todo listo. Puedes empezar el laboratorio 00."
+ exit 0
+fi
+
+echo "$fallas verificación(es) fallaron. Revisa laboratorio/00-preparacion.md."
+exit 1
\ No newline at end of file
diff --git a/src/FinancialUtils/Calculator.cs b/src/FinancialUtils/Calculator.cs
index 09f6202..40fdeeb 100644
--- a/src/FinancialUtils/Calculator.cs
+++ b/src/FinancialUtils/Calculator.cs
@@ -10,6 +10,8 @@ public static class Calculator
///
public static decimal Add(decimal a, decimal b) => a + b;
+ ///public static int Add(int a, int b) => a + b;
+
///
/// Resta b de a.
///
@@ -27,7 +29,9 @@ public static class Calculator
public static decimal Divide(decimal a, decimal b)
{
if (b == 0)
+ {
throw new DivideByZeroException("El divisor no puede ser cero.");
+ }
return a / b;
}
@@ -44,13 +48,19 @@ public static decimal Divide(decimal a, decimal b)
public static decimal CompoundInterest(decimal principal, decimal rate, int periods)
{
if (principal < 0)
+ {
throw new ArgumentException("El capital no puede ser negativo.", nameof(principal));
+ }
if (rate < 0)
+ {
throw new ArgumentException("La tasa no puede ser negativa.", nameof(rate));
+ }
if (periods < 1)
+ {
throw new ArgumentException("Los periodos deben ser un entero positivo.", nameof(periods));
+ }
return principal * (decimal)Math.Pow((double)(1 + rate), periods);
}
@@ -67,16 +77,24 @@ public static decimal CompoundInterest(decimal principal, decimal rate, int peri
public static decimal LoanPayment(decimal principal, decimal annualRate, int months)
{
if (principal <= 0)
+ {
throw new ArgumentException("El préstamo debe ser mayor a cero.", nameof(principal));
+ }
if (annualRate < 0)
+ {
throw new ArgumentException("La tasa no puede ser negativa.", nameof(annualRate));
+ }
if (months < 1)
+ {
throw new ArgumentException("El plazo debe ser un entero positivo.", nameof(months));
+ }
if (annualRate == 0)
+ {
return Math.Round(principal / months, 2, MidpointRounding.AwayFromZero);
+ }
var monthlyRate = annualRate / 12;
var factor = (decimal)Math.Pow((double)(1 + monthlyRate), months);
@@ -101,7 +119,9 @@ public static decimal NetPresentValue(decimal discountRate, IEnumerable
?? throw new ArgumentNullException(nameof(cashFlows));
if (flows.Count == 0)
+ {
throw new ArgumentException("Se requiere al menos un flujo de caja.", nameof(cashFlows));
+ }
decimal npv = 0;
for (int t = 0; t < flows.Count; t++)
diff --git a/src/FinancialUtils/Formatter.cs b/src/FinancialUtils/Formatter.cs
index 25a942f..aa7e617 100644
--- a/src/FinancialUtils/Formatter.cs
+++ b/src/FinancialUtils/Formatter.cs
@@ -17,7 +17,9 @@ public static class Formatter
public static string FormatCurrency(decimal amount, string currencyCode = "USD", string cultureName = "es-MX")
{
if (string.IsNullOrWhiteSpace(currencyCode))
+ {
throw new ArgumentException("El código de moneda no puede estar vacío.", nameof(currencyCode));
+ }
var culture = new CultureInfo(cultureName);
var regionInfo = new RegionInfo(cultureName);
@@ -36,7 +38,9 @@ public static string FormatCurrency(decimal amount, string currencyCode = "USD",
public static string FormatPercentage(decimal value, int decimalPlaces = 2)
{
if (decimalPlaces < 0)
+ {
throw new ArgumentException("Los decimales no pueden ser negativos.", nameof(decimalPlaces));
+ }
return $"{(value * 100).ToString($"F{decimalPlaces}")}%";
}
@@ -50,7 +54,9 @@ public static string FormatPercentage(decimal value, int decimalPlaces = 2)
public static string FormatNumber(decimal value, int decimalPlaces = 0, string cultureName = "es-MX")
{
if (decimalPlaces < 0)
+ {
throw new ArgumentException("Los decimales no pueden ser negativos.", nameof(decimalPlaces));
+ }
var culture = new CultureInfo(cultureName);
return value.ToString($"N{decimalPlaces}", culture);
@@ -64,7 +70,9 @@ public static string FormatNumber(decimal value, int decimalPlaces = 0, string c
public static decimal TruncateDecimals(decimal value, int decimalPlaces)
{
if (decimalPlaces < 0)
+ {
throw new ArgumentException("Los decimales no pueden ser negativos.", nameof(decimalPlaces));
+ }
var factor = (decimal)Math.Pow(10, decimalPlaces);
return Math.Truncate(value * factor) / factor;
diff --git a/tests/FinancialUtils.Tests/CalculatorTests.cs b/tests/FinancialUtils.Tests/CalculatorTests.cs
index 6bab0b2..97dfa17 100644
--- a/tests/FinancialUtils.Tests/CalculatorTests.cs
+++ b/tests/FinancialUtils.Tests/CalculatorTests.cs
@@ -148,10 +148,10 @@ public void LoanPayment_InvalidMonths_ThrowsArgumentException(int months)
public void NetPresentValue_CalculatesCorrectly()
{
// Inversión inicial -1000, flujos futuros de 400 por 3 periodos a tasa 10%
- // NPV = -1000 + 400/1.1 + 400/1.21 + 400/1.331 ≈ -0.64
+ // NPV = -1000 + 400/1.1 + 400/1.21 + 400/1.331 ≈ -5.26
var cashFlows = new[] { -1000m, 400m, 400m, 400m };
var result = Calculator.NetPresentValue(0.10m, cashFlows);
- Assert.Equal(-0.64m, result);
+ Assert.Equal(-5.26m, result);
}
[Fact]
diff --git a/tests/FinancialUtils.Tests/GlobalUsings.cs b/tests/FinancialUtils.Tests/GlobalUsings.cs
index 8c927eb..c802f44 100644
--- a/tests/FinancialUtils.Tests/GlobalUsings.cs
+++ b/tests/FinancialUtils.Tests/GlobalUsings.cs
@@ -1 +1 @@
-global using Xunit;
\ No newline at end of file
+global using Xunit;