name: Build and Deploy LSFE on: push: branches: [ main ] jobs: build-and-deploy: runs-on: win-runner-01 steps: - name: Checkout uses: actions/checkout@v4 - name: Clean previous publish output shell: pwsh run: | Remove-Item -Recurse -Force "C:\ci-output\webapi" -ErrorAction SilentlyContinue - name: Publish WebApi shell: pwsh run: dotnet publish .\LSFE.API\LSFE.API.csproj -c Release -o C:\ci-output\webapi # ---- Generate production config from Gitea Actions secrets (never committed to git) ---- - name: Write production appsettings - WebApi shell: pwsh env: GROQ_KEY: ${{ secrets.LLI_SFE_PROD_GROQ_API_KEY }} JWT_SECRET: ${{ secrets.LLI_SFE_PROD_JWT_SECRET }} DB_CONN: ${{ secrets.LLI_SFE_PROD_DB_CONNECTION }} run: | $config = @{ Groq = @{ ApiKey = $env:GROQ_KEY ApiUrl = "https://api.groq.com/openai/v1/chat/completions" Model = "llama-3.1-8b-instant" } JWT = @{ ValidAudience = "https://lloydwebapi.lloydlab.com:2021" ValidIssuer = "https://lloydwebapi.lloydlab.com:2021" Secret = $env:JWT_SECRET } ConnectionStrings = @{ DefaultConnection = $env:DB_CONN } } $json = $config | ConvertTo-Json -Depth 5 $json | Out-File -FilePath "C:\ci-output\webapi\appsettings.Production.json" -Encoding utf8 Write-Host "Wrote appsettings.Production.json to webapi output (secret values not echoed)" exit 0 # ---- Backup current live deployment before touching anything ---- # NOTE: this backs up Content too (harmless - it's just a point-in-time copy # and is never used to restore Content, see rollback step below). - name: Backup current live files shell: pwsh run: | $stamp = Get-Date -Format "yyyyMMdd-HHmmss" New-Item -ItemType Directory -Force -Path "C:\backups\$stamp" | Out-Null if (Test-Path "C:\inetpub\LSFE-api") { robocopy "C:\inetpub\LSFE-api" "C:\backups\$stamp\webapi" /MIR /R:2 /W:3 /XD Content | Out-Null } $stamp | Out-File -FilePath "C:\backups\latest.txt" -Encoding ascii -NoNewline # Keep only the last 5 backups to avoid filling the disk $all = Get-ChildItem "C:\backups" -Directory | Sort-Object Name -Descending if ($all.Count -gt 5) { $all | Select-Object -Skip 5 | Remove-Item -Recurse -Force } Write-Host "Backed up current deployment to C:\backups\$stamp (Content excluded - never backed up or restored)" exit 0 - name: Stop app pools shell: pwsh run: | Import-Module WebAdministration Stop-WebAppPool -Name "LSFE-Api" -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 - name: Deploy WebApi files id: deploy_api shell: pwsh run: | # Deploy everything EXCEPT the Content folder (and its subfolders, Images/Uploads). # /XD Content excludes the directory by NAME, which robocopy matches reliably # against the source tree it walks, at any depth - so wwwroot\Content and # everything under it (Images, Uploads) is never copied over or deleted. # This prevents /MIR from purging/replacing the live C:\inetpub\LSFE-api\wwwroot\Content folder. robocopy "C:\ci-output\webapi" "C:\inetpub\LSFE-api" /MIR /R:3 /W:5 /XD Content $rc = $LASTEXITCODE Write-Host "ROBOCOPY EXIT CODE: $rc" if ($rc -ge 8) { throw "robocopy failed for WebApi with exit code $rc" } exit 0 - name: Start app pools shell: pwsh run: | Import-Module WebAdministration Start-WebAppPool -Name "LSFE-Api" - name: Verify app pools are running shell: pwsh run: | Start-Sleep -Seconds 3 Import-Module WebAdministration $api = Get-WebAppPoolState -Name "LSFE-Api" Write-Host "LSFE-Api: $($api.Value)" if ($api.Value -ne "Started") { throw "One or more app pools failed to start" } # ---- Rollback path: only runs if any prior step in this job failed ---- - name: ROLLBACK - restore previous backup if: failure() shell: pwsh run: | $stamp = (Get-Content "C:\backups\latest.txt" -Raw).Trim() $backupPath = "C:\backups\$stamp" Write-Host "Deployment failed - rolling back to backup: $backupPath" Import-Module WebAdministration Stop-WebAppPool -Name "LSFE-Api" -ErrorAction SilentlyContinue Start-Sleep -Seconds 3 if (Test-Path "$backupPath\webapi") { # /XD Content here is critical: the backup was taken BEFORE this deploy # attempt, so it holds a stale snapshot of Content. Without this exclusion, # rolling back would blow away any files/uploads written to Content since # the backup was made. Content must never be touched by CI/CD, including rollback. robocopy "$backupPath\webapi" "C:\inetpub\LSFE-api" /MIR /R:3 /W:5 /XD Content | Out-Null } Start-WebAppPool -Name "LSFE-Api" Write-Host "Rollback complete. Restored from $backupPath (Content left untouched)" exit 0 - name: ROLLBACK - verify pools after restore if: failure() shell: pwsh run: | Start-Sleep -Seconds 3 Import-Module WebAdministration $api = Get-WebAppPoolState -Name "LSFE-Api" Write-Host "After rollback - LSFE-Api: $($api.Value)" if ($api.Value -ne "Started") { Write-Host "WARNING: app pool still not running after rollback. Manual intervention needed." }