Automate app-base injection during FTP deploy
This commit is contained in:
2
IIS.md
2
IIS.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user