diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 7d5a552..ac5e11d 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,13 +1,13 @@ -# Gitea Actions CI/CD — build + deploy SOLUTION_ERP lên IIS Windows Server. -# Trigger: push vào branch main, hoặc manual. +# Gitea Actions CI/CD - build + deploy SOLUTION_ERP to IIS on same VPS. +# Trigger: push to main, or manual dispatch. # -# Chạy trên Windows self-hosted runner (vì cần IIS + Word COM cho .doc convert optional). -# Secrets cần set trong Gitea repo settings: -# - IIS_HOST (hostname hoặc IP) -# - IIS_USER (Windows user có admin + WinRM) -# - IIS_PASSWORD -# - JWT_SECRET (64+ chars random — dùng trong appsettings.Production.json) -# - DB_CONNECTION (connection string production) +# 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 @@ -17,102 +17,105 @@ on: workflow_dispatch: jobs: - build-backend: + build-deploy: runs-on: windows-latest steps: - uses: actions/checkout@v4 - - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Restore + Build BE - run: | - dotnet restore SolutionErp.slnx - dotnet build SolutionErp.slnx --no-restore --configuration Release - - - name: Publish Api - run: > - dotnet publish src/Backend/SolutionErp.Api/SolutionErp.Api.csproj - --no-build --configuration Release - --output artifacts/api - --runtime win-x64 --self-contained false - - - uses: actions/upload-artifact@v4 - with: - name: backend-api - path: artifacts/api/ - - build-fe-admin: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: 'fe-admin/.nvmrc' # pin 20.x (gotcha NamGroup) - cache: 'npm' - cache-dependency-path: 'fe-admin/package-lock.json' - - run: npm ci - working-directory: fe-admin - - run: npm run build - working-directory: fe-admin - - uses: actions/upload-artifact@v4 - with: - name: fe-admin-dist - path: fe-admin/dist/ - - build-fe-user: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: 'fe-user/.nvmrc' - cache: 'npm' - cache-dependency-path: 'fe-user/package-lock.json' - - run: npm ci - working-directory: fe-user - - run: npm run build - working-directory: fe-user - - uses: actions/upload-artifact@v4 - with: - name: fe-user-dist - path: fe-user/dist/ - - deploy-iis: - needs: [build-backend, build-fe-admin, build-fe-user] - runs-on: windows-latest - if: github.ref == 'refs/heads/main' - steps: - - uses: actions/download-artifact@v4 - with: - path: artifacts/ - - - name: Zip artifacts + - name: Add dotnet + node to PATH shell: pwsh run: | - Compress-Archive -Path artifacts/backend-api/* -DestinationPath api.zip -Force - Compress-Archive -Path artifacts/fe-admin-dist/* -DestinationPath fe-admin.zip -Force - Compress-Archive -Path artifacts/fe-user-dist/* -DestinationPath fe-user.zip -Force + echo "C:\Program Files\dotnet" >> $env:GITHUB_PATH + echo "C:\Program Files\nodejs" >> $env:GITHUB_PATH - - name: Copy + deploy via WinRM + - name: Show tool versions + shell: pwsh + run: | + & 'C:\Program Files\dotnet\dotnet.exe' --version + & 'C:\Program Files\nodejs\node.exe' --version + & 'C:\Program Files\nodejs\npm.cmd' --version + + - name: Build backend + shell: pwsh + 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 + + - name: Build fe-admin + shell: pwsh + working-directory: fe-admin + run: | + & 'C:\Program Files\nodejs\npm.cmd' ci + & 'C:\Program Files\nodejs\npm.cmd' run build + + - name: Build fe-user + shell: pwsh + working-directory: fe-user + run: | + & 'C:\Program Files\nodejs\npm.cmd' ci + & 'C:\Program Files\nodejs\npm.cmd' run build + + - name: Deploy to IIS (local) + if: github.ref == 'refs/heads/main' shell: pwsh env: - IIS_HOST: ${{ secrets.IIS_HOST }} - IIS_USER: ${{ secrets.IIS_USER }} - IIS_PASSWORD: ${{ secrets.IIS_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + DB_CONNECTION: ${{ secrets.DB_CONNECTION }} run: | - $secure = ConvertTo-SecureString $env:IIS_PASSWORD -AsPlainText -Force - $cred = New-Object PSCredential($env:IIS_USER, $secure) - $session = New-PSSession -ComputerName $env:IIS_HOST -Credential $cred + Import-Module WebAdministration - Copy-Item -Path api.zip, fe-admin.zip, fe-user.zip -Destination "C:\Deploy\" -ToSession $session - - Invoke-Command -Session $session -ScriptBlock { - & C:\Deploy\scripts\deploy-iis.ps1 -Artifact "C:\Deploy\api.zip" -Site "SolutionErpApi" - Expand-Archive "C:\Deploy\fe-admin.zip" "C:\inetpub\solution-erp\fe-admin" -Force - Expand-Archive "C:\Deploy\fe-user.zip" "C:\inetpub\solution-erp\fe-user" -Force + # Stop app pool so DLLs are writable + if (Get-WebAppPoolState -Name SolutionErp-Api -ErrorAction SilentlyContinue) { + Stop-WebAppPool -Name SolutionErp-Api + Start-Sleep -Seconds 3 } - Remove-PSSession $session + # 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 template + secrets + $example = 'C:\inetpub\solution-erp\api\appsettings.Production.json.example' + $prod = 'C:\inetpub\solution-erp\api\appsettings.Production.json' + if (Test-Path $example) { + $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 + } else { + Write-Error "Template $example not found" + exit 1 + } + + # 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: pwsh + run: | + Start-Sleep -Seconds 10 + try { + $r = Invoke-WebRequest -Uri 'https://api.huypham.vn/health/live' -TimeoutSec 30 -UseBasicParsing + Write-Host "API /health/live -> $($r.StatusCode)" + } catch { + Write-Warning "API smoke test: $_" + }