Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" function Require-Tool { param( [Parameter(Mandatory = $true)][string]$Name ) if ($null -eq (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "Required tool not found: $Name" } } function Invoke-NativeCommand { param( [Parameter(Mandatory = $true)][string]$Name, [Parameter(Mandatory = $true)][scriptblock]$Action ) Write-Host $Name & $Action if ($LASTEXITCODE -ne 0) { throw "Step failed: $Name" } } function Get-RemoteScript { param( [Parameter(Mandatory = $true)][string]$RemoteReleaseDir, [Parameter(Mandatory = $true)][string]$RemoteCurrentLink, [Parameter(Mandatory = $true)][string]$ContainerName, [Parameter(Mandatory = $true)][string]$ImageName, [Parameter(Mandatory = $true)][string]$ReleaseTimestamp, [Parameter(Mandatory = $true)][string]$RemoteDataDir, [Parameter(Mandatory = $true)][string]$ContainerPort, [Parameter(Mandatory = $true)][string]$HostPort ) $script = @' set -euo pipefail remote_release_dir='__REMOTE_RELEASE_DIR__' remote_current_link='__REMOTE_CURRENT_LINK__' container_name='__CONTAINER_NAME__' image_name='__IMAGE_NAME__' release_timestamp='__RELEASE_TIMESTAMP__' remote_data_dir='__REMOTE_DATA_DIR__' container_port='__CONTAINER_PORT__' host_port='__HOST_PORT__' previous_current_target='' if [ -L "${remote_current_link}" ]; then previous_current_target="$(readlink -f "${remote_current_link}")" fi docker build -t "${image_name}:${release_timestamp}" -t "${image_name}:latest" "${remote_release_dir}" ln -sfn "${remote_release_dir}" "${remote_current_link}" if docker ps -aq --filter "name=^/${container_name}$" | grep -q .; then docker rm -f "${container_name}" >/dev/null fi if ! docker run -d \ --name "${container_name}" \ --restart unless-stopped \ -p "127.0.0.1:${host_port}:${container_port}" \ -e ASPNETCORE_ENVIRONMENT=Production \ -e ASPNETCORE_URLS="http://+:${container_port}" \ -e ConnectionStrings__RpgRoller="Data Source=/app/data/rpgroller.db" \ -v "${remote_data_dir}:/app/data" \ "${image_name}:${release_timestamp}" >/dev/null; then if [ -n "${previous_current_target}" ]; then ln -sfn "${previous_current_target}" "${remote_current_link}" fi exit 1 fi '@ return $script. Replace("__REMOTE_RELEASE_DIR__", $RemoteReleaseDir). Replace("__REMOTE_CURRENT_LINK__", $RemoteCurrentLink). Replace("__CONTAINER_NAME__", $ContainerName). Replace("__IMAGE_NAME__", $ImageName). Replace("__RELEASE_TIMESTAMP__", $ReleaseTimestamp). Replace("__REMOTE_DATA_DIR__", $RemoteDataDir). Replace("__CONTAINER_PORT__", $ContainerPort). Replace("__HOST_PORT__", $HostPort) } $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = Split-Path -Parent $scriptDir $projectPath = Join-Path $repoRoot "RpgRoller\RpgRoller.csproj" $remoteHost = "myvserver" $remoteRoot = "/root/docker/rpgroller" $remoteReleasesDir = "$remoteRoot/releases" $remoteCurrentLink = "$remoteRoot/current" $remoteDataDir = "$remoteRoot/data" $containerName = "rpgroller" $imageName = "rpgroller" $containerPort = "8080" $hostPort = "8082" $releaseTimestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMddHHmmss") $localStageDir = Join-Path $repoRoot "artifacts\deploy\$releaseTimestamp" $localPublishDir = Join-Path $localStageDir "publish" $remoteReleaseDir = "$remoteReleasesDir/$releaseTimestamp" Write-Host "Deploying release $releaseTimestamp" Require-Tool -Name "dotnet" Require-Tool -Name "ssh" Require-Tool -Name "scp" try { New-Item -ItemType Directory -Path $localPublishDir -Force | Out-Null Invoke-NativeCommand -Name "1) Publishing app locally..." -Action { dotnet publish $projectPath -c Release -o $localPublishDir } @' FROM mcr.microsoft.com/dotnet/aspnet:10.0 WORKDIR /app ENV ASPNETCORE_URLS=http://+:8080 ENV DOTNET_EnableDiagnostics=0 EXPOSE 8080 COPY publish/ ./ RUN mkdir -p /app/data ENTRYPOINT ["dotnet", "RpgRoller.dll"] '@ | Set-Content -Path (Join-Path $localStageDir "Dockerfile") -NoNewline Invoke-NativeCommand -Name "2) Preparing remote release directory..." -Action { ssh $remoteHost "mkdir -p '$remoteReleasesDir' '$remoteDataDir' && test ! -e '$remoteReleaseDir' && mkdir -p '$remoteReleaseDir'" } Invoke-NativeCommand -Name "3) Uploading release payload..." -Action { Push-Location $localStageDir try { scp -r "Dockerfile" "publish" "${remoteHost}:$remoteReleaseDir/" } finally { Pop-Location } } $remoteScript = Get-RemoteScript ` -RemoteReleaseDir $remoteReleaseDir ` -RemoteCurrentLink $remoteCurrentLink ` -ContainerName $containerName ` -ImageName $imageName ` -ReleaseTimestamp $releaseTimestamp ` -RemoteDataDir $remoteDataDir ` -ContainerPort $containerPort ` -HostPort $hostPort Invoke-NativeCommand -Name "4) Building image and restarting container on remote host..." -Action { $remoteScript | ssh $remoteHost "bash -se" } Write-Host "5) Deployment complete." } finally { if (Test-Path $localStageDir) { Remove-Item -Path $localStageDir -Recurse -Force -ErrorAction SilentlyContinue } }