Files
VibeFinance/release.ps1
T
Jordy HoogeveenandClaude Opus 5 081820dfd6 release.ps1 respecteert core.sshCommand bij de origin-controle
Het script zette GIT_SSH_COMMAND hard op "ssh", waarmee het de al
geconfigureerde core.sshCommand overschreef. Op Windows praat alleen de
OpenSSH van het systeem met de ssh-agent; Git's eigen ssh niet. Afhankelijk
van de PATH-volgorde faalde de controle daardoor met "Permission denied"
terwijl git zelf prima kon pushen.

Nu wordt core.sshCommand als basis gebruikt (met "ssh" als fallback) en
worden alleen BatchMode en ConnectTimeout toegevoegd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 23:40:24 +02:00

408 lines
20 KiB
PowerShell

# ── VibeFinance release script (PowerShell) ──────────────────────────────
# Productie release voor de 2-container opzet: draait de tests, bouwt het
# gecombineerde app-image multi-platform, pusht naar de registry, maakt een
# Git-tag en publiceert de release in Gitea.
#
# Gebruik:
# .\release.ps1 # test + bouw + push + Git-tag + Gitea-release
# .\release.ps1 -NoBuild # sla docker build over, alleen tag + release
# .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
# .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren
# .\release.ps1 -Force # sla conflictcontrole over, overschrijf tags
#
# Vereisten:
# - Docker Desktop actief
# - Ingelogd op de registry: docker login <registry>
# - docker buildx builder actief: docker buildx create --use
# - Git geinstalleerd en working directory is de repo root
# - root package.json bevat een schoon versienummer ZONDER -dev suffix
# - GITEA_TOKEN in de omgeving (token met release-rechten) voor stap 5,
# de Gitea-release waar de update-melding in de app op afgaat
param(
[switch]$NoBuild,
[switch]$SkipTests,
[switch]$DryRun,
[switch]$Force
)
$ErrorActionPreference = "Stop"
# ════════════════════════════════════════════════════════════════════════════
# ── PROJECTCONFIGURATIE — pas alleen dit blok aan voor een nieuw project ──
# ════════════════════════════════════════════════════════════════════════════
$appName = "VibeFinance"
$registry = "vibehoogie.duckdns.org/vibe"
$appImage = "vibefinance-app"
$apiUrl = "/api"
$platforms = "linux/amd64,linux/arm64" # arm64 = Raspberry Pi
# ════════════════════════════════════════════════════════════════════════════
# ── Versie uit root package.json ───────────────────────────────────────────
$rootPkg = Get-Content (Join-Path $PSScriptRoot "package.json") -Raw | ConvertFrom-Json
$appVersion = $rootPkg.version
if (-not $appVersion) {
Write-Error "Kon versienummer niet lezen uit package.json"
exit 1
}
if ($appVersion -match "-dev$") {
Write-Error "Versienummer '$appVersion' bevat '-dev' suffix. Verwijder de suffix voor een productie-release."
exit 1
}
# ── Versie synchroniseren naar frontend/package.json ──────────────────────
$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json"
$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json
if ($frontendPkg.version -ne $appVersion) {
$frontendPkg.version = $appVersion
[System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10))
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
}
$gitTag = "$appVersion"
# ── Registry tags (één image voor frontend + backend) ─────────────────────
$appBase = "$registry/$appImage"
$appVersionTag = "$appBase`:$appVersion"
$appLatestTag = "$appBase`:latest"
# ── Helpers ────────────────────────────────────────────────────────────────
function Invoke-Cmd {
param(
[string[]]$Cmd,
# Wordt bij een fout uitgevoerd om een gerichte uitleg te geven
[scriptblock]$Uitleg
)
$display = $Cmd -join " "
if ($DryRun) {
Write-Host "[DRY-RUN] $display" -ForegroundColor Yellow
return ""
}
Write-Host "> $display" -ForegroundColor DarkGray
$output = & $Cmd[0] $Cmd[1..($Cmd.Length - 1)]
if ($LASTEXITCODE -ne 0) {
# Geen 'throw': dat levert een PowerShell-stacktrace met het hele
# commando erin, wat de eigenlijke oorzaak wegdrukt.
Write-Host ""
Write-Host "Mislukt: $($Cmd[0]) $($Cmd[1]) (exitcode $LASTEXITCODE)" -ForegroundColor Red
if ($Uitleg) { & $Uitleg }
Write-Host ""
Write-Host "Er is niets gepubliceerd. Los het bovenstaande op en draai release.ps1 opnieuw." -ForegroundColor DarkGray
exit 1
}
return $output
}
function Write-Step {
param([string]$Label)
Write-Host ""
Write-Host "── $Label" -ForegroundColor Cyan
}
# Haalt de sectie van deze versie uit Changelog.md als release-notes.
function Get-ChangelogSection {
param([string]$Version)
$path = Join-Path $PSScriptRoot "Changelog.md"
if (-not (Test-Path $path)) { return "" }
$lines = Get-Content $path
$start = -1
$needle = "^##\s*\[" + [regex]::Escape($Version) + "\]"
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match $needle) { $start = $i + 1; break }
}
if ($start -lt 0) { return "" }
$out = @()
for ($i = $start; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match "^##\s*\[") { break }
if ($lines[$i] -match "^---\s*$") { continue }
$out += $lines[$i]
}
return (($out -join "`n").Trim())
}
# ── Samenvatting ───────────────────────────────────────────────────────────
Write-Host ""
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
Write-Host " $appName Release" -ForegroundColor Cyan
Write-Host " Versie : $appVersion" -ForegroundColor Green
Write-Host " Git-tag : $gitTag" -ForegroundColor Green
Write-Host " Image : $appVersionTag" -ForegroundColor Green
Write-Host " Platform: $platforms" -ForegroundColor Green
if ($DryRun) { Write-Host " Mode : DRY-RUN (geen wijzigingen)" -ForegroundColor Yellow }
if ($NoBuild) { Write-Host " Mode : SKIP BUILD" -ForegroundColor Yellow }
if ($SkipTests) { Write-Host " Mode : SKIP TESTS" -ForegroundColor Yellow }
if ($Force) { Write-Host " Mode : FORCE (conflictcontrole overgeslagen)" -ForegroundColor Red }
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
# ── Stap 1: Conflictcontrole ──────────────────────────────────────────────
Write-Step "[1/5] Controles (tags + registry-login)"
# Geen prompts: anders blijft het script hangen wanneer SSH om een wachtwoord
# vraagt. Wel de al geconfigureerde ssh gebruiken (core.sshCommand) — op
# Windows praat alleen de OpenSSH van het systeem met de ssh-agent, Git's
# eigen ssh niet.
$sshBasis = (git config --get core.sshCommand)
if (-not $sshBasis) { $sshBasis = "ssh" }
$env:GIT_SSH_COMMAND = "$sshBasis -o BatchMode=yes -o ConnectTimeout=10"
function Test-GitPushToegang {
git ls-remote --heads origin 2>&1 | Out-Null
return ($LASTEXITCODE -eq 0)
}
if (-not $DryRun -and -not $Force) {
$localTag = git tag --list $gitTag
if ($localTag) {
Write-Error "Git-tag '$gitTag' bestaat al lokaal. Verhoog het versienummer in package.json of gebruik -Force."
exit 1
}
# Toegang tot origin nu controleren; stap 4 pusht de tag en zou anders pas
# na de build stranden.
if (-not (Test-GitPushToegang)) {
$remoteUrl = git remote get-url origin
Write-Host ""
Write-Host "Geen toegang tot origin ($remoteUrl)." -ForegroundColor Red
Write-Host ""
Write-Host "Stap 4 pusht de Git-tag, dus dit moet eerst werken. Controleer met:" -ForegroundColor Yellow
Write-Host " git ls-remote --heads origin" -ForegroundColor White
Write-Host ""
Write-Host "Bij 'Permission denied (publickey)' doet de SSH-sleutel het niet." -ForegroundColor Yellow
Write-Host "Snelste alternatief is https, met hetzelfde Gitea-token als de registry:" -ForegroundColor Yellow
Write-Host " git remote set-url origin https://<host>/<org>/<repo>.git" -ForegroundColor White
Write-Host ""
Write-Host "Er is niets gepubliceerd." -ForegroundColor DarkGray
exit 1
}
$remoteTags = git ls-remote --tags origin "refs/tags/$gitTag"
if ($remoteTags) {
Write-Error "Git-tag '$gitTag' bestaat al op origin. Verhoog het versienummer in package.json of gebruik -Force."
exit 1
}
Write-Host "Controleer registry: $appVersionTag" -ForegroundColor DarkGray
$ErrorActionPreference = "SilentlyContinue"
docker manifest inspect $appVersionTag 2>$null | Out-Null
$imageExists = $LASTEXITCODE -eq 0
$ErrorActionPreference = "Stop"
if ($imageExists) {
Write-Error "Registry-tag '$appVersionTag' bestaat al. Verhoog het versienummer in package.json of gebruik -Force."
exit 1
}
Write-Host "Geen conflicten gevonden." -ForegroundColor Green
} elseif ($Force) {
Write-Host "Conflictcontrole overgeslagen (-Force)" -ForegroundColor Yellow
} else {
Write-Host "[DRY-RUN] Conflictcontrole overgeslagen" -ForegroundColor Yellow
}
# ── Registry-login ─────────────────────────────────────────────────────────
# Zonder geldige credentials klapt `buildx --push` pas ná de hele build met
# "401 Unauthorized". Daarom vooraf controleren, en zelf inloggen wanneer er
# een token in de omgeving staat.
$registryHost = $registry.Split("/")[0]
function Test-RegistryLogin {
param([string]$Repo)
# imagetools inspect praat rechtstreeks met de registry: 401 = niet
# ingelogd, "not found" = wél ingelogd maar tag bestaat (nog) niet.
$uitvoer = (docker buildx imagetools inspect "${Repo}:latest" 2>&1 | Out-String)
if ($uitvoer -match "401|[Uu]nauthorized|authentication required") { return $false }
return $true
}
function Connect-Registry {
$gebruiker = $env:REGISTRY_USER
$token = if ($env:REGISTRY_TOKEN) { $env:REGISTRY_TOKEN } else { $env:GITEA_TOKEN }
if (-not ($gebruiker -and $token)) { return $false }
Write-Host "Inloggen op $registryHost als $gebruiker..." -ForegroundColor White
# Via stdin, zodat het token niet in de commandoregel of logs belandt.
$token | docker login $registryHost -u $gebruiker --password-stdin
return ($LASTEXITCODE -eq 0)
}
if (-not $NoBuild) {
if (Test-RegistryLogin $appBase) {
Write-Host "Registry-login in orde ($registryHost)." -ForegroundColor Green
} elseif (Connect-Registry) {
Write-Host "Ingelogd op $registryHost." -ForegroundColor Green
} elseif ($DryRun) {
Write-Host "[DRY-RUN] Niet ingelogd op $registryHost — een echte push zou hier falen." -ForegroundColor Yellow
} else {
Write-Host ""
Write-Host "Niet ingelogd op $registryHost — de push zou met 401 Unauthorized falen." -ForegroundColor Red
Write-Host ""
Write-Host "Kies een van deze twee:" -ForegroundColor Yellow
Write-Host ""
Write-Host " 1. Eenmalig handmatig inloggen (credential blijft bewaard):" -ForegroundColor Yellow
Write-Host " docker login $registryHost" -ForegroundColor White
Write-Host " Gebruikersnaam = je Gitea-account, wachtwoord = een Gitea access" -ForegroundColor DarkGray
Write-Host " token met scope write:package (Settings > Applications > Generate" -ForegroundColor DarkGray
Write-Host " New Token)." -ForegroundColor DarkGray
Write-Host ""
Write-Host " 2. Het script laten inloggen door deze variabelen te zetten:" -ForegroundColor Yellow
Write-Host ' $env:REGISTRY_USER = "<gitea-gebruiker>"' -ForegroundColor White
Write-Host ' $env:REGISTRY_TOKEN = "<gitea-token met write:package>"' -ForegroundColor White
Write-Host ""
Write-Host "Let op: zonder poortnummer inloggen, en in dezelfde shell en onder" -ForegroundColor DarkGray
Write-Host "dezelfde Windows-gebruiker als waarin je release.ps1 draait." -ForegroundColor DarkGray
Write-Host ""
Write-Host "Er is niets gepubliceerd." -ForegroundColor DarkGray
exit 1
}
}
# ── Stap 2: Tests ─────────────────────────────────────────────────────────
if (-not $SkipTests) {
Write-Step "[2/5] Testsuite"
if ($DryRun) {
Write-Host "[DRY-RUN] test.ps1 -NoSmoke" -ForegroundColor Yellow
} else {
& (Join-Path $PSScriptRoot "test.ps1") -NoSmoke
if ($LASTEXITCODE -ne 0) {
Write-Error "Tests mislukt — release afgebroken."
exit 1
}
}
} else {
Write-Step "[2/5] Tests overgeslagen (-SkipTests)"
}
# ── Stap 3: Bouwen en pushen ──────────────────────────────────────────────
if (-not $NoBuild) {
Write-Step "[3/5] Multi-platform bouwen en pushen"
Invoke-Cmd @(
"docker", "buildx", "build",
"--platform", $platforms,
"--target", "production",
"--push",
"-t", $appVersionTag,
"-t", $appLatestTag,
"--build-arg", "APP_VERSION=$appVersion",
"--build-arg", "VITE_API_URL=$apiUrl",
"--build-arg", "VITE_APP_VERSION=$appVersion",
$PSScriptRoot
) -Uitleg {
# Meestal is de build zelf goed en weigert de registry de push.
if (-not (Test-RegistryLogin $appBase)) {
Write-Host "Oorzaak: geen geldige login op $registryHost (de push geeft dan 401 Unauthorized)." -ForegroundColor Yellow
Write-Host ""
Write-Host " docker login $registryHost" -ForegroundColor White
Write-Host " gebruikersnaam : je Gitea-account" -ForegroundColor DarkGray
Write-Host " wachtwoord : Gitea access token met scope write:package" -ForegroundColor DarkGray
Write-Host " (Gitea -> Settings -> Applications -> Generate New Token)" -ForegroundColor DarkGray
Write-Host ""
Write-Host "Of laat het script inloggen:" -ForegroundColor Yellow
Write-Host ' $env:REGISTRY_USER = "<gitea-gebruiker>"' -ForegroundColor White
Write-Host ' $env:REGISTRY_TOKEN = "<gitea-token>"' -ForegroundColor White
} else {
Write-Host "De registry-login is in orde, dus dit is een build- of netwerkfout." -ForegroundColor Yellow
Write-Host "Bekijk de regels boven deze melding; de eerste ERROR wijst de oorzaak aan." -ForegroundColor DarkGray
}
}
} else {
Write-Step "[3/5] Bouwen overgeslagen (-NoBuild)"
}
# ── Stap 4: Git-tag aanmaken en pushen ────────────────────────────────────
Write-Step "[4/5] Git-tag aanmaken en pushen"
if ($Force) {
Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
$ErrorActionPreference = "SilentlyContinue"
git tag -d $gitTag 2>$null | Out-Null
$ErrorActionPreference = "Stop"
Write-Host "Bestaande remote tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
$ErrorActionPreference = "SilentlyContinue"
git push origin ":refs/tags/$gitTag" 2>$null | Out-Null
$ErrorActionPreference = "Stop"
}
Write-Host "Git-tag aanmaken: $gitTag" -ForegroundColor White
Invoke-Cmd @("git", "tag", "-a", $gitTag, "-m", "Release $gitTag")
Write-Host "Git-tag pushen naar origin" -ForegroundColor White
Invoke-Cmd @("git", "push", "origin", "refs/tags/$gitTag") -Uitleg {
Write-Host "Kon de tag niet naar origin pushen." -ForegroundColor Yellow
Write-Host "Het image staat al in de registry; alleen de tag en de Gitea-release ontbreken." -ForegroundColor DarkGray
Write-Host "Controleer de toegang met: git ls-remote --heads origin" -ForegroundColor White
Write-Host "Bij 'Permission denied (publickey)' is https het snelste alternatief:" -ForegroundColor DarkGray
Write-Host " git remote set-url origin https://<host>/<org>/<repo>.git" -ForegroundColor White
}
# ── Stap 5: Gitea-release aanmaken ────────────────────────────────────────
Write-Step "[5/5] Gitea-release aanmaken"
# De update-melding in de app leest /releases/latest. Een kale Git-tag komt
# daar niet in terug, dus er moet een echte release van gemaakt worden.
$repoUrl = ($rootPkg.repository -replace "/+$", "")
$releaseApi = $null
if ($repoUrl -match "^(https?://[^/]+)/([^/]+)/([^/]+)$") {
$releaseApi = "$($Matches[1])/api/v1/repos/$($Matches[2])/$($Matches[3])/releases"
}
$giteaToken = $env:GITEA_TOKEN
if (-not $releaseApi) {
Write-Host "Kon 'repository' niet parsen uit package.json — release overgeslagen." -ForegroundColor Yellow
} elseif (-not $giteaToken) {
Write-Host "Geen GITEA_TOKEN in de omgeving — release overgeslagen." -ForegroundColor Yellow
Write-Host " Zonder release ziet de app geen update-melding voor $appVersion." -ForegroundColor DarkGray
} else {
$notes = Get-ChangelogSection $appVersion
if (-not $notes) { $notes = "Release $appVersion" }
$payload = @{
tag_name = $gitTag
name = "v$appVersion"
body = $notes
draft = $false
prerelease = $false
} | ConvertTo-Json -Depth 3
if ($DryRun) {
Write-Host "[DRY-RUN] POST $releaseApi" -ForegroundColor Yellow
Write-Host $payload -ForegroundColor DarkGray
} else {
Write-Host "Release publiceren: $releaseApi" -ForegroundColor White
try {
$rel = Invoke-RestMethod -Method Post -Uri $releaseApi -Body $payload `
-ContentType "application/json" `
-Headers @{ Authorization = "token $giteaToken" }
Write-Host "Release aangemaakt: $($rel.html_url)" -ForegroundColor Green
} catch {
$code = $_.Exception.Response.StatusCode.value__
if ($code -eq 409) {
Write-Host "Release voor $gitTag bestaat al — overgeslagen." -ForegroundColor Yellow
} else {
Write-Host "Release aanmaken mislukt ($code): $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Tag en image staan al klaar; maak de release eventueel handmatig aan." -ForegroundColor DarkGray
}
}
}
}
# ── Klaar ─────────────────────────────────────────────────────────────────
$kop = if ($DryRun) { "DRY-RUN klaar: $appVersion (niets gepubliceerd)" } else { "Gepubliceerd: $appVersion" }
Write-Host ""
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
Write-Host " $kop" -ForegroundColor Green
Write-Host ""
Write-Host " Registry image:" -ForegroundColor Green
Write-Host " $appVersionTag" -ForegroundColor White
Write-Host " $appLatestTag" -ForegroundColor White
Write-Host ""
Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
Write-Host ""
# Expliciete 0: anders erft de caller de exitcode van het laatste
# docker-commando (bijv. de registry-check).
exit 0