De release strandde pas na een volledige multi-arch build op "401 Unauthorized" van de registry, en bleef daarvoor minuten hangen op een SSH-wachtwoordprompt bij de tag-controle. Beide checks staan nu vooraan. - Stap 1 heet nu "Controles (tags + registry-login)" en test achtereenvolgens: bestaat de tag al lokaal, is origin bereikbaar, bestaat de tag al op origin, bestaat het image al, en is er een geldige registry-login - Registry-login: wanneer REGISTRY_USER en REGISTRY_TOKEN (of GITEA_TOKEN) gezet zijn logt het script zelf in via --password-stdin, anders stopt het met instructies. dev.ps1 push doet dezelfde controle - Git draait met GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=10", dus geen hangende prompts meer; bij geen toegang een uitleg met het https-alternatief - Scripts eindigen expliciet met exit 0 (anders lekt de exitcode van het laatste docker-commando door), en een -DryRun meldt in de slotregel dat er niets is gepubliceerd - BEHEER.md: registry-login met token en de REGISTRY_USER/REGISTRY_TOKEN route Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
365 lines
16 KiB
PowerShell
365 lines
16 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)
|
|
$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) { throw "Commando mislukt: $display" }
|
|
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)"
|
|
|
|
# Git praat zonder prompt met origin: anders blijft het script hangen op een
|
|
# wachtwoordvraag wanneer de SSH-sleutel of -agent niet werkt.
|
|
$env:GIT_SSH_COMMAND = "ssh -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)) {
|
|
Write-Host ""
|
|
Write-Error @"
|
|
Geen toegang tot origin ($(git remote get-url origin)).
|
|
|
|
Controleer het met:
|
|
git ls-remote --heads origin
|
|
|
|
Bij "Permission denied (publickey)": de SSH-sleutel doet het niet. Snelste
|
|
alternatief is pushen over https met hetzelfde Gitea-token als de registry:
|
|
git remote set-url origin https://<host>/<org>/<repo>.git
|
|
"@
|
|
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-Error @"
|
|
Niet ingelogd op $registryHost — de push zou met 401 Unauthorized falen.
|
|
|
|
Kies één van deze twee:
|
|
|
|
1. Eenmalig handmatig inloggen (credential blijft bewaard):
|
|
docker login $registryHost
|
|
Gebruikersnaam = je Gitea-account, wachtwoord = een Gitea access token
|
|
met scope write:package (Settings -> Applications -> Generate New Token).
|
|
|
|
2. Het script laten inloggen door deze variabelen te zetten:
|
|
`$env:REGISTRY_USER = "<gitea-gebruiker>"
|
|
`$env:REGISTRY_TOKEN = "<gitea-token met write:package>"
|
|
|
|
Let op: inloggen op $registryHost zonder poortnummer, en in dezelfde shell
|
|
en onder dezelfde Windows-gebruiker als waarin je release.ps1 draait.
|
|
"@
|
|
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
|
|
)
|
|
} 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")
|
|
|
|
# ── 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
|