# Battani güncelleme betiği (Windows + Docker Desktop) — https://battani.dev/update.ps1 # # irm https://battani.dev/update.ps1 | iex # & ([scriptblock]::Create((irm https://battani.dev/update.ps1))) -Version 1.2.3 # # Parametreler (ortam değişkeni karşılığı parantez içinde): # -Version X.Y.Z Hedef sürüm; varsayılan en yeni sürüm (BATTANI_VERSION) # -Dir YOL Kurulum klasörü; varsayılan %USERPROFILE%\Battani (BATTANI_DIR) # -NoBackup Veritabanı yedeği alma (BATTANI_BACKUP=0) # -Force Aynı sürüm olsa da yeniden başlat (BATTANI_FORCE=1) # -Yes Onay sorma (BATTANI_YES=1) # # Sıra: yeni imajı indir → PostgreSQL yedeği al (backups\) → sürümü .env'de # değiştir → servisleri yeniden oluştur → sağlık kontrolü. Yeni sürüm açılmazsa # eski sürüme geri döner. Veritabanı geçişleri yeni sürümün ilk açılışında # çalışır; geri dönüş sonrası sorun olursa alınan yedekten dönülebilir. # Gövde kendi kapsamında çalışır: `irm | iex` ile de kullanıcının oturumuna # değişken ya da ErrorActionPreference sızmaz. @args parametreleri iletir. & { param( [string]$Version = $env:BATTANI_VERSION, [string]$Dir = $(if ($env:BATTANI_DIR) { $env:BATTANI_DIR } else { Join-Path $HOME 'Battani' }), [switch]$NoBackup = ($env:BATTANI_BACKUP -eq '0'), [switch]$Force = ($env:BATTANI_FORCE -eq '1'), [switch]$Yes = ($env:BATTANI_YES -eq '1') ) $ErrorActionPreference = 'Stop' $PortalUrl = if ($env:BATTANI_PORTAL_URL) { $env:BATTANI_PORTAL_URL } else { 'https://battani.dev' } $SkipPull = $env:BATTANI_SKIP_PULL -eq '1' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 function Say([string]$m) { Write-Host '==> ' -ForegroundColor Green -NoNewline; Write-Host $m } function Warn([string]$m) { Write-Host "!! $m" -ForegroundColor Yellow } function Fail([string]$m) { Write-Host "HATA: $m" -ForegroundColor Red; throw $m } function Confirm-Step([string]$q) { if ($Yes) { return $true } $a = Read-Host "$q [E/h]" return -not ($a -match '^(h|n|hayir|hayır|no)$') } function Invoke-Docker([string[]]$DockerArgs, [switch]$Quiet) { $old = $ErrorActionPreference; $ErrorActionPreference = 'Continue' try { if ($Quiet) { & docker @DockerArgs *> $null } else { & docker @DockerArgs 2>&1 | ForEach-Object { "$_" } | Out-Host } return $LASTEXITCODE } finally { $ErrorActionPreference = $old } } function Get-DockerOutput([string[]]$DockerArgs) { $old = $ErrorActionPreference; $ErrorActionPreference = 'Continue' try { $out = & docker @DockerArgs 2>$null if ($LASTEXITCODE -ne 0) { return $null } return ($out | Out-String).Trim() } finally { $ErrorActionPreference = $old } } $envFile = Join-Path $Dir '.env' function Get-EnvValue([string]$Key) { $line = Get-Content $envFile -Encoding UTF8 | Where-Object { $_ -like "$Key=*" } | Select-Object -Last 1 if ($line) { return $line.Substring($Key.Length + 1) } else { return '' } } # UTF-8 (BOM'suz) ve LF korunarak tek anahtarı değiştirir. function Set-EnvValue([string]$Key, [string]$Value) { $text = [System.IO.File]::ReadAllText($envFile) $pattern = "(?m)^$([regex]::Escape($Key))=.*$" if ($text -match $pattern) { $text = [regex]::Replace($text, $pattern, "$Key=$Value") } else { $text = $text.TrimEnd("`n") + "`n$Key=$Value`n" } [System.IO.File]::WriteAllText($envFile, $text, (New-Object System.Text.UTF8Encoding $false)) } function Get-LatestVersion([string]$Image) { if (-not $Image.StartsWith('ghcr.io/')) { return $null } $repo = $Image.Substring('ghcr.io/'.Length) try { $token = (Invoke-RestMethod -UseBasicParsing "https://ghcr.io/token?scope=repository:${repo}:pull").token $tags = (Invoke-RestMethod -UseBasicParsing -Headers @{ Authorization = "Bearer $token" } "https://ghcr.io/v2/$repo/tags/list?n=10000").tags } catch { return $null } $semver = @($tags | Where-Object { $_ -match '^\d+\.\d+\.\d+$' }) if ($semver.Count -eq 0) { return $null } return ($semver | Sort-Object { [version]$_ } | Select-Object -Last 1) } function Wait-Healthy([string]$Container, [int]$Seconds) { $elapsed = 0 while ($elapsed -lt $Seconds) { $info = Get-DockerOutput @('inspect', '-f', '{{.State.Status}} {{.RestartCount}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}', $Container) if (-not $info) { return $false } $state, $restarts, $health = $info -split ' ' if ($health -eq 'healthy') { return $true } if ($state -in @('exited', 'dead')) { return $false } if ([int]$restarts -ge 3) { return $false } Start-Sleep -Seconds 5 $elapsed += 5 } return $false } Write-Host '' Write-Host 'Battani güncelleme' -ForegroundColor White Write-Host '' if (-not ((Test-Path $envFile) -and (Test-Path (Join-Path $Dir 'docker-compose.yml')))) { Fail "$Dir içinde kurulum bulunamadı. Kurmak için: irm $PortalUrl/install.ps1 | iex" } if ((Invoke-Docker @('info') -Quiet) -ne 0) { Fail 'Docker çalışmıyor. Docker Desktop''ı başlatıp tekrar deneyin.' } $Image = Get-EnvValue 'BATTANI_IMAGE' $Current = Get-EnvValue 'BATTANI_IMAGE_TAG' if (-not $Image) { Fail '.env içinde BATTANI_IMAGE yok; bu kurulum install betiğiyle yapılmamış.' } $Version = "$Version".TrimStart('v') if (-not $Version) { Say 'En yeni sürüm aranıyor…' $Version = Get-LatestVersion $Image if (-not $Version) { Fail 'En yeni sürüm bulunamadı; -Version ile belirtin.' } } Say "Kurulu sürüm: $Current → hedef: $Version" if ($Version -eq $Current -and -not $Force) { Say 'Zaten güncel. (Yeniden başlatmak için -Force)'; return } if (-not (Confirm-Step 'Güncelleme başlasın mı?')) { Fail 'İptal edildi.' } if (-not $SkipPull) { Say "Yeni imaj indiriliyor: ${Image}:$Version" if ((Invoke-Docker @('pull', '--quiet', "${Image}:$Version") -Quiet) -ne 0) { Fail "İmaj indirilemedi: ${Image}:$Version" } } $composeArgs = @('compose', '--project-directory', $Dir) $backupFile = $null if (-not $NoBackup) { $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss') $backupFile = Join-Path (Join-Path $Dir 'backups') "battani-$stamp-$Current.sql.gz" New-Item -ItemType Directory -Force -Path (Join-Path $Dir 'backups') | Out-Null Say "Veritabanı yedeği alınıyor: $backupFile" # Yedek container içinde sıkıştırılıp `docker cp` ile alınır: PowerShell 5.1 # ikili çıktıyı boru hattında bozar. pipefail, pg_dump hatasını gizlemez. # Tırnak yok: PowerShell 5.1 native argümanlardaki çift tırnağı bozar; değerler # kurulumun ürettiği düz adlardır. $dump = 'set -o pipefail; pg_dump -U $POSTGRES_USER -d $POSTGRES_DB --no-owner | gzip > /tmp/battani-backup.sql.gz' $ok = (Invoke-Docker ($composeArgs + @('exec', '-T', 'postgres', 'sh', '-c', $dump))) -eq 0 if ($ok) { $ok = (Invoke-Docker @('cp', 'battani-postgres:/tmp/battani-backup.sql.gz', $backupFile) -Quiet) -eq 0 } Invoke-Docker @('exec', 'battani-postgres', 'rm', '-f', '/tmp/battani-backup.sql.gz') -Quiet | Out-Null if (-not $ok -or -not (Test-Path $backupFile) -or (Get-Item $backupFile).Length -lt 100) { Remove-Item $backupFile -ErrorAction SilentlyContinue Fail 'Yedek alınamadı; güncelleme yapılmadı. (Yedeksiz devam için -NoBackup)' } } # Eski kurulumlar: lisans kurulum kimliği için bilgisayar kimliği bağlantısını ekle. $composeFile = Join-Path $Dir 'docker-compose.yml' $composeText = [System.IO.File]::ReadAllText($composeFile) if ($composeText -notmatch 'host-machine-id') { Say 'Bilgisayar kimliği bağlantısı ekleniyor (lisans kurulum kimliği için)…' $hostIdFile = Join-Path $Dir 'host-id' if (-not (Test-Path $hostIdFile)) { $machineGuid = $null try { $machineGuid = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid } catch { } if (-not $machineGuid) { $machineGuid = [guid]::NewGuid().ToString() } [System.IO.File]::WriteAllText($hostIdFile, $machineGuid, (New-Object System.Text.UTF8Encoding $false)) } if (-not (Get-EnvValue 'BATTANI_HOST_ID_FILE')) { Set-EnvValue 'BATTANI_HOST_ID_FILE' './host-id' } $composeText = $composeText -replace '(?m)^ - app-config:/app/config\r?$', " - app-config:/app/config`n - `${BATTANI_HOST_ID_FILE}:/etc/host-machine-id:ro" [System.IO.File]::WriteAllText($composeFile, $composeText, (New-Object System.Text.UTF8Encoding $false)) } # Eski kurulumlar: elle girilen veri servisi kimlik bilgileri için şifreleme anahtarı. if (-not (Get-EnvValue 'INTEGRATION_SECRETS_KEY')) { $chars = [char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' $bytes = New-Object byte[] 64 $rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider $rng.GetBytes($bytes); $rng.Dispose() Set-EnvValue 'INTEGRATION_SECRETS_KEY' (-join ($bytes | ForEach-Object { $chars[$_ % 62] })) } $composeFile2 = Join-Path $Dir 'docker-compose.yml' $composeText2 = [System.IO.File]::ReadAllText($composeFile2) if ($composeText2 -notmatch 'INTEGRATION_SECRETS_KEY') { $composeText2 = $composeText2 -replace '(?m)^ REGISTRY_SECRETS_KEY: \$\{REGISTRY_SECRETS_KEY\}\r?$', " REGISTRY_SECRETS_KEY: `${REGISTRY_SECRETS_KEY}`n INTEGRATION_SECRETS_KEY: `${INTEGRATION_SECRETS_KEY}" [System.IO.File]::WriteAllText($composeFile2, $composeText2, (New-Object System.Text.UTF8Encoding $false)) } Say 'Sürüm değiştiriliyor ve servisler yeniden oluşturuluyor…' Set-EnvValue 'BATTANI_IMAGE_TAG' $Version if ((Invoke-Docker ($composeArgs + @('up', '-d'))) -ne 0) { Warn 'docker compose up hata verdi; uygulamanın durumu kontrol ediliyor…' } Say 'Uygulamanın açılması bekleniyor (veritabanı geçişleri çalışıyor)…' if (Wait-Healthy 'battani-app' 300) { Write-Host '' Write-Host "Battani $Version sürümüne güncellendi." -ForegroundColor Green if ($backupFile) { Write-Host " Yedek: $backupFile" } Write-Host ' Eski imajları temizlemek için: docker image prune' Write-Host '' return } Warn 'Yeni sürüm sağlıklı açılmadı. Son loglar:' Invoke-Docker @('logs', '--tail', '40', 'battani-app') | Out-Null if ($Current) { Warn "Eski sürüme ($Current) dönülüyor…" Set-EnvValue 'BATTANI_IMAGE_TAG' $Current Invoke-Docker ($composeArgs + @('up', '-d')) | Out-Null if (Wait-Healthy 'battani-app' 300) { Warn "$Current sürümüne geri dönüldü ve çalışıyor." } else { Warn 'Eski sürüm de açılamadı.' } } if ($backupFile) { Warn "Gerekirse veritabanını güncelleme öncesine döndürmek için yedek: $backupFile" } Fail "Güncelleme başarısız. Loglar: docker compose --project-directory `"$Dir`" logs -f app" } @args