From 6e5bbec86e092325fd21a0e5479f473935fa50d4 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Mon, 9 Feb 2026 18:46:52 +0100 Subject: [PATCH] Automate app-base injection during FTP deploy --- IIS.md | 2 +- README.md | 1 + scripts/deploy-ftp.profile.sample.psd1 | 1 + scripts/deploy-ftp.ps1 | 84 ++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/IIS.md b/IIS.md index 997d8e1..93ff5d3 100644 --- a/IIS.md +++ b/IIS.md @@ -21,7 +21,7 @@ - `AllowedHosts=picknplay.example.com;www.picknplay.example.com` - Optional: enable stdout logging in `web.config` during troubleshooting only; disable afterward. - Data protection keys are persisted to `App_Data/keys`; ensure this folder is deployed and writable so auth cookies stay valid across app pool recycles. -- Frontend base path: set `` in `wwwroot/index.html` for production so API calls include the subpath (keep blank for local/root). +- Frontend base path is injected during deployment by `scripts/deploy-ftp.ps1` using deploy profile `BasePath` (falls back to last `RemoteDir` segment if omitted). This keeps local `wwwroot/index.html` unchanged while production API calls target `/picknplay/api`. - Deployment script: copy `scripts/deploy-ftp.profile.sample.psd1` to `scripts/deploy-ftp.profile.psd1`, fill environment values, then run `pwsh ./scripts/deploy-ftp.ps1 -ProfilePath ./scripts/deploy-ftp.profile.psd1`. - Shortcut command: run `pwsh ./deploy.ps1` from repo root to deploy with the local profile directly. - Prefer `WinScpSessionName` in the deploy profile to avoid embedding FTP credentials in scripted URLs. diff --git a/README.md b/README.md index ed8e0e5..c371c32 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Pick'n'Play is a .NET 10 ASP.NET Core Minimal API app with a static HTML/CSS/JS - `GameList.Tests/`: integration and helper tests. - `scripts/`: deployment scripts (`scripts/deploy-ftp.ps1`, `scripts/deploy-ftp1.ps1`). - `deploy.ps1`: local shortcut wrapper that runs FTP deploy using `scripts/deploy-ftp.profile.psd1`. + Deploy sets frontend `` automatically from deploy profile `BasePath` (or inferred from `RemoteDir`). ## Operations diff --git a/scripts/deploy-ftp.profile.sample.psd1 b/scripts/deploy-ftp.profile.sample.psd1 index 5e20b98..bcce81e 100644 --- a/scripts/deploy-ftp.profile.sample.psd1 +++ b/scripts/deploy-ftp.profile.sample.psd1 @@ -9,6 +9,7 @@ # Required sync settings WinScpPath = "C:\Program Files (x86)\WinSCP\WinSCP.com" RemoteDir = "/httpdocs/picknplay" + BasePath = "/picknplay" # Preferred: use a named WinSCP stored session (no credential string in script) WinScpSessionName = "picknplay-prod" diff --git a/scripts/deploy-ftp.ps1 b/scripts/deploy-ftp.ps1 index bdea593..48c1c7e 100644 --- a/scripts/deploy-ftp.ps1 +++ b/scripts/deploy-ftp.ps1 @@ -54,6 +54,86 @@ function Resolve-ProfilePath { return [System.IO.Path]::GetFullPath((Join-Path $BaseDirectory $expanded)) } +function Normalize-BasePath { + param([string]$Value) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return "" + } + + $normalized = $Value.Trim() + if (-not $normalized.StartsWith("/")) { + $normalized = "/$normalized" + } + + if ($normalized.Length -gt 1) { + $normalized = $normalized.TrimEnd("/") + } + + return $normalized +} + +function Infer-BasePathFromRemoteDir { + param([string]$RemoteDir) + + if ([string]::IsNullOrWhiteSpace($RemoteDir)) { + return "" + } + + $segments = @($RemoteDir -split "[/\\]" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + if ($segments.Count -eq 0) { + return "" + } + + $candidate = $segments[$segments.Count - 1] + if ($candidate -in @("httpdocs", "wwwroot", "www", "public_html", "site")) { + return "" + } + + return Normalize-BasePath $candidate +} + +function Resolve-AppBasePath { + param([Parameter(Mandatory = $true)][hashtable]$Config) + + if ($Config.ContainsKey("BasePath")) { + $configured = Normalize-BasePath ([string]$Config.BasePath) + if (-not [string]::IsNullOrWhiteSpace($configured)) { + return $configured + } + } + + return Infer-BasePathFromRemoteDir ([string]$Config.RemoteDir) +} + +function Set-FrontendAppBaseMeta { + param( + [Parameter(Mandatory = $true)][string]$PublishDir, + [Parameter(Mandatory = $true)][string]$BasePath + ) + + $indexPath = Join-Path $PublishDir "index.html" + if (-not (Test-Path $indexPath)) { + throw "Publish output is missing index.html at '$indexPath'." + } + + $pattern = '' + $content = Get-Content -Path $indexPath -Raw + if ($content -notmatch $pattern) { + throw "Could not find in '$indexPath'." + } + + $replacement = "" + $updated = [System.Text.RegularExpressions.Regex]::Replace( + $content, + $pattern, + [System.Text.RegularExpressions.MatchEvaluator]{ param($match) $replacement }, + 1 + ) + + Set-Content -Path $indexPath -Value $updated -Encoding UTF8 +} + function Read-PlainOrPrompt { param( [string]$Value, @@ -174,6 +254,10 @@ if (-not $selfContained) { } dotnet @publishArgs +$appBasePath = Resolve-AppBasePath -Config $config +Set-FrontendAppBaseMeta -PublishDir $publishDir -BasePath $appBasePath +Write-Host "2) Frontend app-base configured as '$appBasePath'." -ForegroundColor Cyan + if ($recycleAppPool) { Require-ConfigValue $config "AppPoolName" $appPoolName = [string]$config.AppPoolName