diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 new file mode 100644 index 0000000..84cb8b7 --- /dev/null +++ b/scripts/deploy.ps1 @@ -0,0 +1,167 @@ +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 + } +}