From c0c2408e6830d82b5baad57e2b89d5c1e05dfc31 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Wed, 26 Aug 2026 22:09:12 +0900 Subject: デプロイ用スクリプト追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy.ps1 | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 deploy.ps1 (limited to 'deploy.ps1') diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..970bacd --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,314 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$siteRoot = $PSScriptRoot +$configPath = $null +$localArchive = $null +$remoteArchive = $null +$remoteArchiveMayExist = $false +$deploymentSucceeded = $false +$failureMessage = $null +$locationPushed = $false +$nativeCommands = @{} +$sshOptions = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10") + +function Get-NativeCommandPath { + param( + [Parameter(Mandatory)] + [string]$Name + ) + + $command = Get-Command -Name $Name -CommandType Application -ErrorAction Stop + return $command.Source +} + +function Invoke-CheckedNativeCommand { + param( + [Parameter(Mandatory)] + [string]$Command, + + [Parameter()] + [string[]]$Arguments = @(), + + [Parameter(Mandatory)] + [string]$Operation + ) + + & $Command @Arguments + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + throw "$Operation failed with exit code $exitCode." + } +} + +function Invoke-CheckedSshScript { + param( + [Parameter(Mandatory)] + [string]$Command, + + [Parameter(Mandatory)] + [string[]]$Options, + + [Parameter(Mandatory)] + [string]$Target, + + [Parameter(Mandatory)] + [string]$Script + ) + + $Script | & $Command @Options $Target "sh -s" + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + throw "Remote release activation failed with exit code $exitCode." + } +} + +try { + if ($args.Count -gt 0) { + throw "deploy.ps1 does not accept arguments. Edit deploy.config.psd1 instead." + } + + if ([string]::IsNullOrWhiteSpace($siteRoot)) { + throw "Unable to determine the site root. Run deploy.ps1 from a saved file." + } + + $siteRoot = (Resolve-Path -LiteralPath $siteRoot).Path + if (-not (Test-Path -LiteralPath (Join-Path $siteRoot "zola.toml") -PathType Leaf)) { + throw "zola.toml was not found next to deploy.ps1." + } + + $configPath = Join-Path $siteRoot "deploy.config.psd1" + if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { + throw ( + "deploy.config.psd1 was not found. " + + "Copy deploy.config.example.psd1 and enter the local deployment settings." + ) + } + + $deployConfig = Import-PowerShellDataFile -LiteralPath $configPath + if ($deployConfig -isnot [System.Collections.IDictionary]) { + throw "deploy.config.psd1 must contain a PowerShell data hashtable." + } + + $requiredConfigKeys = @( + "DeployHost", + "DeployUser", + "RemoteBase", + "IdentityFile", + "SiteUrl" + ) + foreach ($key in $requiredConfigKeys) { + if (-not $deployConfig.Contains($key)) { + throw "deploy.config.psd1 is missing the required key: $key" + } + } + + $DeployHost = ([string]$deployConfig["DeployHost"]).Trim() + $DeployUser = ([string]$deployConfig["DeployUser"]).Trim() + $RemoteBase = ([string]$deployConfig["RemoteBase"]).Trim() + $IdentityFile = ([string]$deployConfig["IdentityFile"]).Trim() + $siteUrl = ([string]$deployConfig["SiteUrl"]).Trim() + + if ($DeployHost -notmatch '^[A-Za-z0-9._-]+$') { + throw "DeployHost contains unsupported characters." + } + if ($DeployUser -notmatch '^[A-Za-z_][A-Za-z0-9_-]*$') { + throw "DeployUser contains unsupported characters." + } + + $RemoteBase = $RemoteBase.TrimEnd('/') + if ( + $RemoteBase.Length -le 1 -or + $RemoteBase -notmatch '^/[A-Za-z0-9._/-]+$' -or + $RemoteBase -match '(^|/)\.\.?(/|$)' -or + $RemoteBase.Contains('//') + ) { + throw "RemoteBase must be a normalized absolute Unix path other than '/'." + } + if (-not $RemoteBase.StartsWith('/srv/www/', [System.StringComparison]::Ordinal)) { + throw "RemoteBase must be located below /srv/www/." + } + + $parsedSiteUrl = $null + if ( + -not [System.Uri]::TryCreate( + $siteUrl, + [System.UriKind]::Absolute, + [ref]$parsedSiteUrl + ) -or + $parsedSiteUrl.Scheme -ne "https" + ) { + throw "SiteUrl must be an absolute HTTPS URL." + } + + if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) { + $identityCandidate = $IdentityFile + if (-not [System.IO.Path]::IsPathRooted($identityCandidate)) { + $identityCandidate = Join-Path $siteRoot $identityCandidate + } + $identityPath = (Resolve-Path -LiteralPath $identityCandidate -ErrorAction Stop).Path + if (-not (Test-Path -LiteralPath $identityPath -PathType Leaf)) { + throw "IdentityFile must point to a file." + } + $sshOptions += @("-i", $identityPath) + } + + foreach ($name in @("zola", "tar", "ssh", "scp")) { + $nativeCommands[$name] = Get-NativeCommandPath -Name $name + } + + Push-Location -LiteralPath $siteRoot + $locationPushed = $true + + Write-Host "[1/6] Checking site..." + Invoke-CheckedNativeCommand ` + -Command $nativeCommands["zola"] ` + -Arguments @("check") ` + -Operation "zola check" + + Write-Host "[2/6] Building site..." + Invoke-CheckedNativeCommand ` + -Command $nativeCommands["zola"] ` + -Arguments @("build") ` + -Operation "zola build" + + $publicDir = Join-Path $siteRoot "public" + $publicIndex = Join-Path $publicDir "index.html" + if (-not (Test-Path -LiteralPath $publicIndex -PathType Leaf)) { + throw "Build validation failed: public/index.html was not generated." + } + + $resolvedPublicDir = (Resolve-Path -LiteralPath $publicDir).Path + $siteRootPrefix = $siteRoot.TrimEnd( + [System.IO.Path]::DirectorySeparatorChar, + [System.IO.Path]::AltDirectorySeparatorChar + ) + [System.IO.Path]::DirectorySeparatorChar + if (-not $resolvedPublicDir.StartsWith( + $siteRootPrefix, + [System.StringComparison]::OrdinalIgnoreCase + )) { + throw "Resolved public directory is outside the site root." + } + + $releaseId = Get-Date -Format "yyyyMMdd-HHmmss" + $archiveNonce = [System.Guid]::NewGuid().ToString("N") + $archiveName = "takashi-2521-$releaseId-$archiveNonce.tar.gz" + $localArchive = Join-Path ([System.IO.Path]::GetTempPath()) $archiveName + $remoteArchive = "/tmp/$archiveName" + $remoteTarget = "$DeployUser@$DeployHost" + + Write-Host "[3/6] Creating archive..." + Invoke-CheckedNativeCommand ` + -Command $nativeCommands["tar"] ` + -Arguments @("-czf", $localArchive, "-C", $resolvedPublicDir, ".") ` + -Operation "tar archive creation" + + if ( + -not (Test-Path -LiteralPath $localArchive -PathType Leaf) -or + (Get-Item -LiteralPath $localArchive).Length -le 0 + ) { + throw "Archive validation failed: the local archive is missing or empty." + } + + Write-Host "[4/6] Uploading release..." + $scpArguments = @($sshOptions) + @( + $localArchive, + "${remoteTarget}:$remoteArchive" + ) + $remoteArchiveMayExist = $true + Invoke-CheckedNativeCommand ` + -Command $nativeCommands["scp"] ` + -Arguments $scpArguments ` + -Operation "SCP upload" + + $remoteScript = @' +set -eu +umask 022 + +base='__REMOTE_BASE__' +release_id='__RELEASE_ID__' +archive='__REMOTE_ARCHIVE__' +release="$base/releases/$release_id" +new_link="$base/current.new" +release_created=0 + +cleanup() { + status=$? + trap - 0 + rm -f -- "$archive" || true + rm -f -- "$new_link" || true + if [ "$status" -ne 0 ] && [ "$release_created" -eq 1 ]; then + rm -rf -- "$release" || true + fi + exit "$status" +} +trap cleanup 0 + +test -d "$base/releases" +test -L "$base/current" +test -f "$base/current/index.html" +test -f "$archive" + +if [ -e "$release" ] || [ -L "$release" ]; then + echo "Release already exists: $release_id" >&2 + exit 1 +fi + +if [ -e "$new_link" ] || [ -L "$new_link" ]; then + rm -f -- "$new_link" +fi + +mkdir -- "$release" +release_created=1 +tar -xzf "$archive" -C "$release" +test -f "$release/index.html" + +ln -s "releases/$release_id" "$new_link" +mv -Tf "$new_link" "$base/current" +release_created=0 +'@ + $remoteScript = $remoteScript.Replace("__REMOTE_BASE__", $RemoteBase) + $remoteScript = $remoteScript.Replace("__RELEASE_ID__", $releaseId) + $remoteScript = $remoteScript.Replace("__REMOTE_ARCHIVE__", $remoteArchive) + + Write-Host "[5/6] Switching release..." + Invoke-CheckedSshScript ` + -Command $nativeCommands["ssh"] ` + -Options $sshOptions ` + -Target $remoteTarget ` + -Script $remoteScript + + $deploymentSucceeded = $true +} +catch { + $failureMessage = $_.Exception.Message +} +finally { + if ($locationPushed) { + Pop-Location + } + + if ($null -ne $localArchive -and (Test-Path -LiteralPath $localArchive)) { + Remove-Item -LiteralPath $localArchive -Force -ErrorAction SilentlyContinue + } + + if ( + $remoteArchiveMayExist -and + -not $deploymentSucceeded -and + $null -ne $remoteArchive + ) { + $cleanupCommand = "rm -f -- '$remoteArchive'" + & $nativeCommands["ssh"] @sshOptions $remoteTarget $cleanupCommand 2>$null | Out-Null + } +} + +if (-not $deploymentSucceeded) { + [System.Console]::Error.WriteLine("Deployment failed: $failureMessage") + exit 1 +} + +Write-Host "[6/6] Done." +Write-Host "" +Write-Host "Release: $releaseId" +Write-Host "URL: $siteUrl" +exit 0 -- cgit v1.2.3