Files
solution-erp/.gitea/workflows/deploy.yml
pqhuy1987 29eb5d99a0
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 59s
[CLAUDE] CICD: path filter docs-only + npm junction cache (Option C optimize ~2/3 deploy time)
Optimize CI/CD theo Option C bàn trong chat:

==== Path filter (saving 100% time cho commit MD-only) ====
on: push: paths-ignore mới — skip CI khi commit chỉ docs/skill/MD/gitignore.
- 'docs/**'
- '**/*.md'
- '.claude/skills/**'
- '.gitignore'
- 'scripts/**.md'

Commit 'Docs: chốt session' và similar sẽ KHÔNG trigger workflow → save 196s/commit.
Nếu cùng commit thay đổi cả MD + code → vẫn trigger (đúng behavior expected).
Workflow file `.gitea/workflows/**` chính NÓ thì không trong paths-ignore →
vẫn trigger khi sửa CI config (an toàn).

==== npm junction cache (saving ~70-80s code commit) ====
Replace Build fe-admin + fe-user steps với cache-aware version.

Strategy:
- Cache key = SHA256(package.json) 16-char prefix → đổi deps = miss → fresh
- Cache stored: C:\npm-cache-erp\<app>\<hash>\node_modules (ngoài workspace)
- Junction `fe-admin\node_modules → cache` (instant, không file copy)
- Lần đầu (cold): 49s + 33s = 82s (như cũ)
- Lần sau (warm): mklink instant + skip npm install → ~3s + 3s = 6s (saving ~76s)

Safety:
- Trước Deploy: convert junction → nothing (cmd /c rmdir /q chỉ remove ref,
  không follow target). Tránh trường hợp act_runner cleanup workspace
  follow junction + delete cache.
- Pruning: keep top 5 cache per app (~250MB × 5 × 2 = 2.5GB max disk usage).
  Stale evicted FIFO theo LastWriteTime DESC.

Vite 8 rolldown native binding gotcha (#20) vẫn respect: cache install trên
runner Windows nên rolldown binding match → reuse được.

==== Expected ====
- Commit MD-only: 0s CI (skip hoàn toàn)
- Commit code lần đầu sau cache miss (vd npm update): ~3min (như cũ)
- Commit code thường (cache hit): ~120s = 2 phút (giảm 38%)

Verify dotnet test local: 71 pass / 2s (BE không thay đổi).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:24:09 +07:00

271 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Gitea Actions CI/CD - build + deploy SOLUTION_ERP to IIS on same VPS.
# Trigger: push to main, or manual dispatch.
#
# Self-hosted Windows runner on VPS (shared with VIETREPORT). Runner has:
# - git, .NET 10 SDK, Node 20, IIS
# - Can deploy locally (no WinRM needed)
#
# Secrets required in Gitea repo settings:
# - JWT_SECRET (64+ chars random)
# - DB_CONNECTION (full connection string with vrapp password)
name: Deploy SOLUTION_ERP
# Path filter — skip CI khi commit chỉ docs/MD/skill (~110s saved per docs commit).
# Commit MD-only như "Docs: chốt session" sẽ KHÔNG trigger workflow.
# Lưu ý: nếu cùng 1 commit thay đổi cả MD + code → vẫn trigger (đúng behavior).
on:
push:
branches: [main]
paths-ignore:
- 'docs/**'
- '**/*.md'
- '.claude/skills/**'
- '.gitignore'
- 'scripts/**.md'
workflow_dispatch:
jobs:
build-deploy:
runs-on: windows-latest
steps:
# Manual checkout thay vì `uses: actions/checkout@v4` — tránh phụ thuộc
# github.com (act_runner mỗi run đều `git fetch` để check update action,
# khi VPS → github.com TCP timeout 21s thì toàn job fail trước khi tới
# test gate). Gitea internal network luôn ổn định, nên clone trực tiếp.
# Token `${{ github.token }}` (Gitea cũng dùng tên này) tự sẵn cho job.
- name: Checkout (manual git, bypass github.com)
shell: powershell
run: |
git config --global --add safe.directory '*'
git init -q
git remote add origin "https://gitea-actions:${{ github.token }}@git.baocaogiaoduc.vn/${{ github.repository }}.git"
# Fetch ref (branch) thay vì SHA — không cần Gitea allow SHA fetch.
# Depth 30 đủ buffer nếu main đã commit thêm sau khi job pickup.
$ref = "${{ github.ref }}"
if ($ref -like "refs/heads/*") { $ref = $ref.Substring(11) }
git fetch --depth=30 origin $ref
git checkout --quiet "${{ github.sha }}"
git log -1 --oneline
- name: Show tool versions
shell: powershell
run: |
& 'C:\Program Files\dotnet\dotnet.exe' --version
& 'C:\Program Files\nodejs\node.exe' --version
& 'C:\Program Files\nodejs\npm.cmd' --version
# ============== TEST GATE ==============
# Run tests TRƯỚC build/publish/deploy. Fail → exit non-zero → no deploy.
# Phase 1: Domain (54 test policy state machine).
# Phase 2: Infrastructure (17 test code generators format/sequence/year scope).
- name: Run unit tests (Domain)
shell: powershell
run: |
& 'C:\Program Files\dotnet\dotnet.exe' test tests/SolutionErp.Domain.Tests/SolutionErp.Domain.Tests.csproj `
--configuration Release `
--logger "trx;LogFileName=domain-tests.trx" `
--results-directory test-results
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Run integration tests (Infrastructure - SQLite in-memory)
shell: powershell
run: |
& 'C:\Program Files\dotnet\dotnet.exe' test tests/SolutionErp.Infrastructure.Tests/SolutionErp.Infrastructure.Tests.csproj `
--configuration Release `
--logger "trx;LogFileName=infra-tests.trx" `
--results-directory test-results
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# Upload test results — bỏ vì `actions/upload-artifact@v4` cũng phụ thuộc
# github.com fetch (cùng vấn đề như actions/checkout). TRX file vẫn save
# local trong workspace `test-results/` cho debug khi cần.
- name: List test results (local debug)
if: always()
shell: powershell
run: |
if (Test-Path test-results) {
Get-ChildItem test-results -Recurse | Format-Table FullName, Length
} else {
Write-Host "No test-results directory."
}
# ============== BUILD ==============
- name: Build backend
shell: powershell
run: |
& 'C:\Program Files\dotnet\dotnet.exe' restore SolutionErp.slnx
& 'C:\Program Files\dotnet\dotnet.exe' publish src/Backend/SolutionErp.Api/SolutionErp.Api.csproj `
--configuration Release `
--output out/api `
--runtime win-x64 `
--self-contained false
# ============== FE BUILD WITH NPM CACHE ==============
# Cache `node_modules` ở `C:\npm-cache-erp\<app>\<hash>\` ngoài workspace.
# Cache key = SHA256(package.json) 16-char prefix → đổi deps = fresh install.
# Junction từ workspace → cache (instant, không file copy).
# Lần đầu (cold cache): 49s + 33s. Lần sau (warm cache): 1-2s mỗi app.
# Vite 8 rolldown native binding match platform — install trên runner Windows
# (gotcha #20 từ Phase 5) → cache install này dùng được lần sau.
- name: Build fe-admin (with npm junction cache)
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$hash = (Get-FileHash 'fe-admin/package.json' -Algorithm SHA256).Hash.Substring(0, 16)
$cacheDir = "C:\npm-cache-erp\fe-admin\$hash"
$cacheNm = "$cacheDir\node_modules"
$nmTarget = 'fe-admin\node_modules'
# Clean junction/dir cũ trong workspace nếu còn (act_runner thường clean nhưng phòng hờ)
if (Test-Path $nmTarget) { cmd /c rmdir /s /q $nmTarget }
if (Test-Path $cacheNm) {
Write-Host "✓ npm cache HIT fe-admin (key: $hash)"
cmd /c mklink /J $nmTarget $cacheNm | Out-Null
} else {
Write-Host "✗ npm cache MISS fe-admin (key: $hash) — fresh install"
Push-Location fe-admin
try {
Remove-Item package-lock.json -Force -ErrorAction SilentlyContinue
& 'C:\Program Files\nodejs\npm.cmd' install --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { throw "npm install failed: $LASTEXITCODE" }
} finally { Pop-Location }
New-Item -ItemType Directory -Force $cacheDir | Out-Null
Move-Item $nmTarget $cacheNm -Force
cmd /c mklink /J $nmTarget $cacheNm | Out-Null
}
Push-Location fe-admin
try {
& 'C:\Program Files\nodejs\npm.cmd' run build
if ($LASTEXITCODE -ne 0) { throw "npm build failed: $LASTEXITCODE" }
} finally { Pop-Location }
- name: Build fe-user (with npm junction cache)
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$hash = (Get-FileHash 'fe-user/package.json' -Algorithm SHA256).Hash.Substring(0, 16)
$cacheDir = "C:\npm-cache-erp\fe-user\$hash"
$cacheNm = "$cacheDir\node_modules"
$nmTarget = 'fe-user\node_modules'
if (Test-Path $nmTarget) { cmd /c rmdir /s /q $nmTarget }
if (Test-Path $cacheNm) {
Write-Host "✓ npm cache HIT fe-user (key: $hash)"
cmd /c mklink /J $nmTarget $cacheNm | Out-Null
} else {
Write-Host "✗ npm cache MISS fe-user (key: $hash) — fresh install"
Push-Location fe-user
try {
Remove-Item package-lock.json -Force -ErrorAction SilentlyContinue
& 'C:\Program Files\nodejs\npm.cmd' install --no-audit --no-fund
if ($LASTEXITCODE -ne 0) { throw "npm install failed: $LASTEXITCODE" }
} finally { Pop-Location }
New-Item -ItemType Directory -Force $cacheDir | Out-Null
Move-Item $nmTarget $cacheNm -Force
cmd /c mklink /J $nmTarget $cacheNm | Out-Null
}
Push-Location fe-user
try {
& 'C:\Program Files\nodejs\npm.cmd' run build
if ($LASTEXITCODE -ne 0) { throw "npm build failed: $LASTEXITCODE" }
} finally { Pop-Location }
# Safety: convert junction → nothing trước khi act_runner có thể cleanup workspace.
# `cmd /c rmdir /q` chỉ remove junction reference, KHÔNG recurse vào target.
# Tránh trường hợp Remove-Item -Recurse từ runner cleanup follow junction +
# delete cache contents.
- name: Remove FE node_modules junctions (preserve cache)
if: always()
shell: powershell
run: |
foreach ($app in @('fe-admin', 'fe-user')) {
$junction = "$app\node_modules"
if (Test-Path $junction) {
cmd /c rmdir /q $junction 2>$null
Write-Host "Removed junction: $junction"
}
}
# Cache cleanup — keep top 5 most recent per app (FIFO eviction).
# Mỗi cache ~250MB. 5 versions × 2 app × 250MB = 2.5 GB max disk usage.
- name: Prune old npm caches
if: always()
shell: powershell
run: |
foreach ($app in @('fe-admin', 'fe-user')) {
$root = "C:\npm-cache-erp\$app"
if (Test-Path $root) {
$stale = Get-ChildItem $root -Directory |
Sort-Object LastWriteTime -Descending |
Select-Object -Skip 5
foreach ($d in $stale) {
Write-Host "Pruning stale cache: $($d.FullName)"
Remove-Item $d.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
- name: Deploy to IIS (local)
if: github.ref == 'refs/heads/main'
shell: powershell
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
DB_CONNECTION: ${{ secrets.DB_CONNECTION }}
run: |
Import-Module WebAdministration
# Stop app pool (if running) so DLLs are writable
$poolState = (Get-WebAppPoolState -Name SolutionErp-Api -ErrorAction SilentlyContinue).Value
if ($poolState -eq 'Started') {
Stop-WebAppPool -Name SolutionErp-Api
Start-Sleep -Seconds 3
}
# Deploy API
Remove-Item -Path 'C:\inetpub\solution-erp\api\*' -Recurse -Force -Exclude 'appsettings.Production.json','logs','uploads','wwwroot' -ErrorAction SilentlyContinue
Copy-Item -Path 'out\api\*' -Destination 'C:\inetpub\solution-erp\api\' -Recurse -Force
# Write appsettings.Production.json from source template + secrets.
# Template is in source workspace (not in publish output - dotnet publish
# doesn't copy .example files).
$example = 'src\Backend\SolutionErp.Api\appsettings.Production.json.example'
$prod = 'C:\inetpub\solution-erp\api\appsettings.Production.json'
$settings = Get-Content $example -Raw | ConvertFrom-Json
$settings.ConnectionStrings.Default = $env:DB_CONNECTION
$settings.Jwt.Secret = $env:JWT_SECRET
$settings | ConvertTo-Json -Depth 10 | Set-Content -Path $prod -Encoding UTF8
Write-Host "Wrote appsettings.Production.json"
# Restrict ACL
icacls $prod /inheritance:r | Out-Null
icacls $prod /grant:r 'Administrators:(R,W)' 'IIS AppPool\SolutionErp-Api:(R)' | Out-Null
# Deploy fe-admin
Remove-Item -Path 'C:\inetpub\solution-erp\fe-admin\*' -Recurse -Force -Exclude 'web.config' -ErrorAction SilentlyContinue
Copy-Item -Path 'fe-admin\dist\*' -Destination 'C:\inetpub\solution-erp\fe-admin\' -Recurse -Force
# Deploy fe-user
Remove-Item -Path 'C:\inetpub\solution-erp\fe-user\*' -Recurse -Force -Exclude 'web.config' -ErrorAction SilentlyContinue
Copy-Item -Path 'fe-user\dist\*' -Destination 'C:\inetpub\solution-erp\fe-user\' -Recurse -Force
# Restart app pool
Start-WebAppPool -Name SolutionErp-Api
Write-Host "Deploy done. App pool started."
- name: Smoke test
if: github.ref == 'refs/heads/main'
shell: powershell
run: |
Start-Sleep -Seconds 10
try {
$r = Invoke-WebRequest -Uri 'https://api.solutions.com.vn/health/live' -TimeoutSec 30 -UseBasicParsing
Write-Host "API /health/live -> $($r.StatusCode)"
} catch {
Write-Warning "API smoke test: $_"
}