Automate app-base injection during FTP deploy

This commit is contained in:
2026-02-09 18:46:52 +01:00
parent 78dccff90f
commit 6e5bbec86e
4 changed files with 87 additions and 1 deletions

2
IIS.md
View File

@@ -21,7 +21,7 @@
- `AllowedHosts=picknplay.example.com;www.picknplay.example.com` - `AllowedHosts=picknplay.example.com;www.picknplay.example.com`
- Optional: enable stdout logging in `web.config` during troubleshooting only; disable afterward. - 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. - 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 `<meta name="app-base" content="/picknplay">` 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`. - 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. - 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. - Prefer `WinScpSessionName` in the deploy profile to avoid embedding FTP credentials in scripted URLs.

View File

@@ -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. - `GameList.Tests/`: integration and helper tests.
- `scripts/`: deployment scripts (`scripts/deploy-ftp.ps1`, `scripts/deploy-ftp1.ps1`). - `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.ps1`: local shortcut wrapper that runs FTP deploy using `scripts/deploy-ftp.profile.psd1`.
Deploy sets frontend `<meta name="app-base">` automatically from deploy profile `BasePath` (or inferred from `RemoteDir`).
## Operations ## Operations

View File

@@ -9,6 +9,7 @@
# Required sync settings # Required sync settings
WinScpPath = "C:\Program Files (x86)\WinSCP\WinSCP.com" WinScpPath = "C:\Program Files (x86)\WinSCP\WinSCP.com"
RemoteDir = "/httpdocs/picknplay" RemoteDir = "/httpdocs/picknplay"
BasePath = "/picknplay"
# Preferred: use a named WinSCP stored session (no credential string in script) # Preferred: use a named WinSCP stored session (no credential string in script)
WinScpSessionName = "picknplay-prod" WinScpSessionName = "picknplay-prod"

View File

@@ -54,6 +54,86 @@ function Resolve-ProfilePath {
return [System.IO.Path]::GetFullPath((Join-Path $BaseDirectory $expanded)) 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 = '<meta\s+name=["'']app-base["'']\s+content=["''][^"'']*["'']\s*/?>'
$content = Get-Content -Path $indexPath -Raw
if ($content -notmatch $pattern) {
throw "Could not find <meta name=`"app-base`"> in '$indexPath'."
}
$replacement = "<meta name=`"app-base`" content=`"$BasePath`">"
$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 { function Read-PlainOrPrompt {
param( param(
[string]$Value, [string]$Value,
@@ -174,6 +254,10 @@ if (-not $selfContained) {
} }
dotnet @publishArgs 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) { if ($recycleAppPool) {
Require-ConfigValue $config "AppPoolName" Require-ConfigValue $config "AppPoolName"
$appPoolName = [string]$config.AppPoolName $appPoolName = [string]$config.AppPoolName