Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee6d5b9b1e | ||
|
|
6c1c651839 | ||
|
|
795a056564 |
No files matched your search
@@ -1,7 +1,5 @@
|
||||
*.bak
|
||||
*.exe
|
||||
scripts/*
|
||||
!scripts/update-manifests.py
|
||||
.vscode/*
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -0,0 +1,49 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Aktualisiert Scoop und alle benutzerspezifischen Pakete; elexis* bleibt gesperrt.
|
||||
.DESCRIPTION
|
||||
Fuehrt ausschliesslich den Update-Lauf aus. Die Aufgabe wird von update-bucket.ps1
|
||||
eingerichtet und startet dieses Skript verborgen. Der letzte Lauf steht in
|
||||
auto-update.log neben diesem Skript. Globale Pakete werden nicht aktualisiert.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot)
|
||||
$env:SCOOP = $ScoopRoot
|
||||
$scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1'
|
||||
if (-not (Test-Path -LiteralPath $scoopCommand -PathType Leaf)) {
|
||||
throw "Scoop wurde nicht gefunden: $scoopCommand"
|
||||
}
|
||||
|
||||
$transcriptStarted = $false
|
||||
$exitCode = 0
|
||||
try {
|
||||
Start-Transcript -LiteralPath (Join-Path $PSScriptRoot 'auto-update.log') -Force | Out-Null
|
||||
$transcriptStarted = $true
|
||||
# Ein absoluter Scoop-Pfad und die shims im PATH funktionieren auch ohne Profil.
|
||||
$env:PATH = (Join-Path $ScoopRoot 'shims') + [IO.Path]::PathSeparator + $env:PATH
|
||||
$installed = @(& $scoopCommand list)
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Installierte Scoop-Pakete konnten nicht ermittelt werden.' }
|
||||
foreach ($package in $installed) {
|
||||
if ($package.Name -notlike 'elexis*' -or $package.Info -match 'Global install|Install failed') { continue }
|
||||
& $scoopCommand hold $package.Name
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Updatesperre fuer '$($package.Name)' fehlgeschlagen. Kein Update ausgefuehrt."
|
||||
}
|
||||
}
|
||||
& $scoopCommand update
|
||||
if ($LASTEXITCODE -ne 0) { throw 'scoop update fehlgeschlagen.' }
|
||||
& $scoopCommand update '*'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'scoop update * fehlgeschlagen.' }
|
||||
} catch {
|
||||
Write-Error $_ -ErrorAction Continue
|
||||
$exitCode = 1
|
||||
} finally {
|
||||
if ($transcriptStarted) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
exit $exitCode
|
||||
@@ -1,4 +1,4 @@
|
||||
# 1. SCOOP CHECK & INSTALL
|
||||
# 1. SCOOP CHECK & INSTALL
|
||||
$bucketList = scoop bucket list 2>$null | Out-String
|
||||
|
||||
if ($bucketList) {
|
||||
@@ -0,0 +1,69 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Fragt nach und richtet optional die taeglichen Scoop-Auto-Updates ein.
|
||||
.DESCRIPTION
|
||||
Eigenstaendig ausfuehrbar, ohne Bucket-Migration. Bei y wird auto-update.ps1
|
||||
heruntergeladen und eine Aufgabe fuer taeglich 03:00 Uhr erstellt.
|
||||
Die Aufgabe startet das Updateskript verborgen und holt verpasste Termine nach,
|
||||
sobald die Benutzersitzung und eine Netzwerkverbindung verfuegbar sind.
|
||||
Bei n wird nichts eingerichtet; bestehende Aufgaben bleiben unveraendert.
|
||||
.EXAMPLE
|
||||
.\scripts\setup-auto-update.ps1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot)
|
||||
$repository = 'https://gitea.medelexis.ch/public/scoop'
|
||||
|
||||
do {
|
||||
$autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant()
|
||||
} while ($autoUpdateAnswer -notin @('y', 'n'))
|
||||
|
||||
if ($autoUpdateAnswer -eq 'y') {
|
||||
$scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1'
|
||||
if (-not (Test-Path -LiteralPath $scoopCommand -PathType Leaf)) {
|
||||
throw "Scoop wurde nicht gefunden: $scoopCommand"
|
||||
}
|
||||
$autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis'
|
||||
New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null
|
||||
$autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1'
|
||||
$downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid())
|
||||
try {
|
||||
$autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1"
|
||||
Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing
|
||||
$parseTokens = $null
|
||||
$parseErrors = $null
|
||||
$downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors)
|
||||
if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or
|
||||
'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) {
|
||||
throw 'Das heruntergeladene Auto-Update-Skript ist kein gueltiges Updateskript.'
|
||||
}
|
||||
Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force
|
||||
# Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet.
|
||||
$userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
$taskName = "Medelexis-Scoop-AutoUpdate-$userSid"
|
||||
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||
$arguments = '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -ScoopRoot "{1}"' -f $autoUpdateScript, $ScoopRoot
|
||||
$action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments
|
||||
$trigger = New-ScheduledTaskTrigger -Daily -At '03:00'
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable `
|
||||
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 4)
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
|
||||
-Principal $principal -Settings $settings -Force `
|
||||
-Description 'Scoop taeglich um 03:00 aktualisieren; verpasste Termine nachholen; elexis* auf hold setzen.' | Out-Null
|
||||
Write-Host "Scoop Auto-Update eingeschaltet: $taskName (taeglich 03:00, verpasste Termine nachholen)."
|
||||
Write-Host "Ausfuehrung verborgen unter $userId, sobald der Benutzer angemeldet ist; kein Aufwecken des PCs."
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath }
|
||||
}
|
||||
} else {
|
||||
Write-Host 'Auto-Update nicht eingerichtet. Eine eventuell bestehende Aufgabe bleibt unveraendert.'
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stellt bestehende Scoop-Installationen von elexis/streichtech auf medelexis um.
|
||||
.DESCRIPTION
|
||||
Ohne -Apply nur ein Probelauf. Keine Sicherungen oder Git-Pushes.
|
||||
Bearbeitet alle Versionsverzeichnisse der benutzerspezifischen Scoop-Installation.
|
||||
Ein passender bestehender Bucket wird umbenannt. Danach werden elexis und streichtech entfernt.
|
||||
Andere Buckets wie main und extras bleiben bestehen.
|
||||
Alte Paketnamen mit Unterstrich werden auf passende Manifestnamen mit Bindestrich umgestellt.
|
||||
ee-iso/ee_iso und ee-cli werden deinstalliert. MyElexis CLI wird neu installiert.
|
||||
Mit -Apply wird abschliessend gefragt, ob taegliche Auto-Updates um 03:00 Uhr
|
||||
eingerichtet werden sollen. Das separate Skript wird aus dem Repository geladen.
|
||||
.EXAMPLE
|
||||
.\scripts\update-bucket.ps1
|
||||
.EXAMPLE
|
||||
.\scripts\update-bucket.ps1 -Apply
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch] $Apply,
|
||||
[string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repository = 'https://gitea.medelexis.ch/public/scoop'
|
||||
$targetName = 'medelexis'
|
||||
$target = Join-Path $ScoopRoot "buckets\$targetName"
|
||||
$apps = Join-Path $ScoopRoot 'apps'
|
||||
if (-not (Test-Path -LiteralPath $apps -PathType Container)) {
|
||||
throw "Kein Scoop-Apps-Verzeichnis gefunden: $apps"
|
||||
}
|
||||
|
||||
function Test-MedelexisRepository([string] $Path) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return $false }
|
||||
$remote = & git -C $Path remote get-url origin 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { return $false }
|
||||
try { $uri = [Uri] ([string] $remote).Trim() } catch { return $false }
|
||||
return ($uri.Scheme -eq 'https' -and $uri.Host -eq 'gitea.medelexis.ch' -and
|
||||
$uri.AbsolutePath.TrimEnd('/').Replace('.git', '') -eq '/public/scoop')
|
||||
}
|
||||
|
||||
function Get-PackageRenames([string] $BucketPath) {
|
||||
if (-not $BucketPath -or -not (Test-Path -LiteralPath $BucketPath)) { return }
|
||||
foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) {
|
||||
if ($app.Name -in @('ee-iso', 'ee_iso')) { continue }
|
||||
if (-not $app.Name.Contains('_')) { continue }
|
||||
$newName = $app.Name.Replace('_', '-')
|
||||
$newManifest = Join-Path $BucketPath "bucket\$newName.json"
|
||||
if (-not (Test-Path -LiteralPath $newManifest)) {
|
||||
$newManifest = Join-Path $BucketPath "$newName.json"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $newManifest)) { continue }
|
||||
$current = Join-Path $app.FullName 'current'
|
||||
$installFile = Join-Path $current 'install.json'
|
||||
if (-not (Test-Path -LiteralPath $installFile)) { continue }
|
||||
$install = [IO.File]::ReadAllText($installFile) | ConvertFrom-Json
|
||||
if ($install.bucket -notin @('elexis', 'streichtech', 'medelexis')) { continue }
|
||||
$destination = Join-Path $apps $newName
|
||||
if (Test-Path -LiteralPath $destination) {
|
||||
throw "'$($app.Name)' und '$newName' sind beide installiert. Keine automatische Zusammenführung."
|
||||
}
|
||||
$link = Get-Item -LiteralPath $current -Force
|
||||
if ($link.LinkType -ne 'Junction') {
|
||||
throw "Für '$($app.Name)' wird eine Scoop-current-Junction erwartet. Keine automatische Umbenennung."
|
||||
}
|
||||
$versionPath = [IO.Path]::GetFullPath([string] @($link.Target)[0])
|
||||
if ((Split-Path $versionPath -Parent) -ne $app.FullName) {
|
||||
throw "Die current-Verknüpfung von '$($app.Name)' zeigt ausserhalb des Paketordners."
|
||||
}
|
||||
$persistSource = Join-Path $ScoopRoot "persist\$($app.Name)"
|
||||
$persistTarget = Join-Path $ScoopRoot "persist\$newName"
|
||||
if ((Test-Path -LiteralPath $persistSource) -and (Test-Path -LiteralPath $persistTarget)) {
|
||||
throw "Beide Persistenzordner für '$($app.Name)' und '$newName' existieren. Keine automatische Zusammenführung."
|
||||
}
|
||||
Write-Host "Paket umbenennen: $($app.Name) -> $newName"
|
||||
[pscustomobject]@{
|
||||
OldName = $app.Name; NewName = $newName; Source = $app.FullName
|
||||
Destination = $destination; Version = (Split-Path $versionPath -Leaf)
|
||||
PersistSource = $persistSource; PersistTarget = $persistTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$installCli = (Test-Path -LiteralPath (Join-Path $apps 'ee-cli')) -or
|
||||
-not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli'))
|
||||
$obsoleteApps = @(@('ee-iso', 'ee_iso', 'ee-cli') | Where-Object {
|
||||
Test-Path -LiteralPath (Join-Path $apps $_)
|
||||
})
|
||||
if ($installCli -and (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli'))) {
|
||||
$obsoleteApps += 'myelexis-cli'
|
||||
}
|
||||
foreach ($obsolete in $obsoleteApps) {
|
||||
Write-Host "Veraltetes Paket deinstallieren: $obsolete"
|
||||
}
|
||||
if ($installCli) { Write-Host 'MyElexis CLI wird aus medelexis neu installiert.' }
|
||||
|
||||
$renameSource = $null
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
if (-not (Test-MedelexisRepository $target)) {
|
||||
throw 'Der vorhandene Bucket medelexis zeigt auf ein anderes Repository. Keine Änderung.'
|
||||
}
|
||||
Write-Host 'Bucket medelexis ist bereits korrekt eingerichtet.'
|
||||
} else {
|
||||
foreach ($oldName in @('elexis', 'streichtech')) {
|
||||
$candidate = Join-Path $ScoopRoot "buckets\$oldName"
|
||||
if (Test-MedelexisRepository $candidate) {
|
||||
$renameSource = $candidate
|
||||
Write-Host "Bucket umbenennen: $oldName -> $targetName"
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $renameSource) { Write-Host "Bucket hinzufügen: $targetName -> $repository" }
|
||||
}
|
||||
|
||||
$changes = @(
|
||||
foreach ($app in Get-ChildItem -LiteralPath $apps -Directory) {
|
||||
if ($app.Name -in $obsoleteApps) { continue }
|
||||
foreach ($version in Get-ChildItem -LiteralPath $app.FullName -Directory) {
|
||||
# current und andere Verknüpfungen nicht nochmals bearbeiten.
|
||||
if ($version.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue }
|
||||
$file = Join-Path $version.FullName 'install.json'
|
||||
if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue }
|
||||
$original = [IO.File]::ReadAllText($file)
|
||||
$data = $original | ConvertFrom-Json
|
||||
if ($data.bucket -notin @('elexis', 'streichtech')) { continue }
|
||||
$pattern = '(?i)("bucket"\s*:\s*)"(?:elexis|streichtech)"'
|
||||
$updated = [regex]::Replace($original, $pattern, '${1}"medelexis"')
|
||||
if (($updated | ConvertFrom-Json).bucket -ne $targetName) {
|
||||
throw "Bucket-Zuordnung konnte nicht angepasst werden: $file"
|
||||
}
|
||||
Write-Host "$($app.Name) / $($version.Name): $($data.bucket) -> $targetName"
|
||||
[pscustomobject]@{ File = $file; Original = $original; Updated = $updated; App = $app.Name }
|
||||
}
|
||||
}
|
||||
)
|
||||
$manifestRoot = $(if (Test-Path -LiteralPath $target) { $target } else { $renameSource })
|
||||
$renames = @(Get-PackageRenames $manifestRoot)
|
||||
|
||||
if (-not $Apply) {
|
||||
foreach ($oldName in @('elexis', 'streichtech')) {
|
||||
if (Test-Path -LiteralPath (Join-Path $ScoopRoot "buckets\$oldName")) {
|
||||
Write-Host "Alter Bucket wird nach der Umstellung entfernt oder umbenannt: $oldName"
|
||||
}
|
||||
}
|
||||
Write-Host "Probelauf: $($changes.Count) install.json-Dateien würden geändert."
|
||||
Write-Host "Probelauf: $($renames.Count) Paketnamen würden umgestellt."
|
||||
Write-Host "Probelauf: $(@($obsoleteApps).Count) alte Pakete würden deinstalliert."
|
||||
Write-Host 'Mit -Apply wird optional ein Auto-Update um 03:00 Uhr mit Nachholen eingerichtet (y/n).'
|
||||
Write-Host 'Mit -Apply anwenden. Es werden keine Sicherungen erstellt.'
|
||||
return
|
||||
}
|
||||
|
||||
$activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
|
||||
if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) {
|
||||
throw 'ScoopRoot muss der aktiven Scoop-Installation entsprechen.'
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $target)) {
|
||||
if ($renameSource) {
|
||||
$bucketRoot = (Resolve-Path -LiteralPath (Join-Path $ScoopRoot 'buckets')).Path
|
||||
$sourcePath = (Resolve-Path -LiteralPath $renameSource).Path
|
||||
$targetPath = [IO.Path]::GetFullPath($target)
|
||||
if ((Split-Path $sourcePath -Parent) -ne $bucketRoot -or
|
||||
(Split-Path $targetPath -Parent) -ne $bucketRoot) {
|
||||
throw 'Bucket-Pfade liegen ausserhalb des erwarteten Verzeichnisses. Keine Umbenennung.'
|
||||
}
|
||||
[IO.Directory]::Move($sourcePath, $targetPath)
|
||||
} else {
|
||||
# scoop verwendet dieselbe Root-Variable für benutzerdefinierte Installationen.
|
||||
if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne
|
||||
[IO.Path]::GetFullPath($(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })).TrimEnd('\')) {
|
||||
throw 'ScoopRoot weicht von der aktiven Scoop-Installation ab; Bucket bitte dort zuerst einrichten.'
|
||||
}
|
||||
& scoop bucket add $targetName $repository
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $target)) {
|
||||
throw 'Bucket konnte nicht hinzugefügt werden. Installationsmetadaten bleiben unverändert.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Falls der Ziel-Bucket erst hinzugefügt wurde, stehen seine Manifestnamen jetzt bereit.
|
||||
if (-not $manifestRoot) { $renames = @(Get-PackageRenames $target) }
|
||||
|
||||
# Vor dem ersten Schreiben prüfen, ob während des Probelaufs Dateien geändert wurden.
|
||||
foreach ($change in $changes) {
|
||||
if ([IO.File]::ReadAllText($change.File) -cne $change.Original) {
|
||||
throw 'Installationsmetadaten wurden zwischenzeitlich geändert. Skript bitte erneut starten.'
|
||||
}
|
||||
}
|
||||
foreach ($change in $changes) {
|
||||
[IO.File]::WriteAllText($change.File, $change.Updated, [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
if ($installCli -and -not (Test-Path -LiteralPath (Join-Path $target 'bucket\myelexis-cli.json')) -and
|
||||
-not (Test-Path -LiteralPath (Join-Path $target 'myelexis-cli.json'))) {
|
||||
throw 'Manifest myelexis-cli fehlt im Ziel-Bucket. Keine Deinstallation.'
|
||||
}
|
||||
foreach ($obsolete in $obsoleteApps) {
|
||||
$activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
|
||||
if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) {
|
||||
throw 'Für Deinstallationen muss ScoopRoot der aktiven Scoop-Installation entsprechen.'
|
||||
}
|
||||
& scoop uninstall $obsolete
|
||||
if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath (Join-Path $apps $obsolete))) {
|
||||
throw "'$obsolete' konnte nicht deinstalliert werden."
|
||||
}
|
||||
}
|
||||
if ($installCli) {
|
||||
Write-Host 'MyElexis CLI aus medelexis neu installieren ...'
|
||||
& scoop install 'medelexis/myelexis-cli'
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath (Join-Path $apps 'myelexis-cli\current\myelexis-cli.exe'))) {
|
||||
throw 'MyElexis CLI konnte nicht vollständig installiert werden.'
|
||||
}
|
||||
}
|
||||
foreach ($rename in $renames) {
|
||||
$activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
|
||||
if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) {
|
||||
throw 'Für Paketumbenennungen muss ScoopRoot der aktiven Scoop-Installation entsprechen.'
|
||||
}
|
||||
$appsRoot = (Resolve-Path -LiteralPath $apps).Path
|
||||
if ((Split-Path $rename.Source -Parent) -ne $appsRoot -or
|
||||
(Split-Path $rename.Destination -Parent) -ne $appsRoot) {
|
||||
throw 'Paketpfade liegen ausserhalb des Apps-Verzeichnisses.'
|
||||
}
|
||||
if (Test-Path -LiteralPath $rename.PersistSource) {
|
||||
$persistRoot = (Resolve-Path -LiteralPath (Join-Path $ScoopRoot 'persist')).Path
|
||||
if ((Split-Path $rename.PersistSource -Parent) -ne $persistRoot -or
|
||||
(Split-Path $rename.PersistTarget -Parent) -ne $persistRoot) {
|
||||
throw 'Persistenzpfade liegen ausserhalb des Persist-Verzeichnisses.'
|
||||
}
|
||||
[IO.Directory]::Move($rename.PersistSource, $rename.PersistTarget)
|
||||
}
|
||||
$oldCurrent = Join-Path $rename.Source 'current'
|
||||
& attrib -R /L $oldCurrent
|
||||
if ($LASTEXITCODE -ne 0) { throw 'current-Verknüpfung konnte nicht freigegeben werden.' }
|
||||
# Nur die Junction entfernen, ohne rekursiv ihren Inhalt zu löschen.
|
||||
[IO.Directory]::Delete($oldCurrent)
|
||||
[IO.Directory]::Move($rename.Source, $rename.Destination)
|
||||
$newCurrent = Join-Path $rename.Destination 'current'
|
||||
$newVersion = Join-Path $rename.Destination $rename.Version
|
||||
New-Item -ItemType Junction -Path $newCurrent -Target $newVersion | Out-Null
|
||||
Write-Host "Startverknüpfungen für $($rename.NewName) erneuern ..."
|
||||
& scoop reset "$($rename.NewName)@$($rename.Version)"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Scoop reset für '$($rename.NewName)' fehlgeschlagen." }
|
||||
}
|
||||
foreach ($oldName in @('elexis', 'streichtech')) {
|
||||
$oldPath = Join-Path $ScoopRoot "buckets\$oldName"
|
||||
if (-not (Test-Path -LiteralPath $oldPath)) { continue }
|
||||
$activeRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' })
|
||||
if ([IO.Path]::GetFullPath($ScoopRoot).TrimEnd('\') -ne [IO.Path]::GetFullPath($activeRoot).TrimEnd('\')) {
|
||||
throw 'Zum Entfernen alter Buckets muss ScoopRoot der aktiven Scoop-Installation entsprechen.'
|
||||
}
|
||||
Write-Host "Alten Bucket entfernen: $oldName"
|
||||
& scoop bucket rm $oldName
|
||||
if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath $oldPath)) {
|
||||
throw "Alter Bucket '$oldName' konnte nicht entfernt werden. Die Umstellung kann erneut ausgeführt werden."
|
||||
}
|
||||
}
|
||||
foreach ($name in @((Get-ChildItem -LiteralPath $apps -Directory).Name)) {
|
||||
$installFile = Join-Path $apps "$name\current\install.json"
|
||||
if (-not (Test-Path -LiteralPath $installFile)) { continue }
|
||||
if (([IO.File]::ReadAllText($installFile) | ConvertFrom-Json).bucket -ne $targetName) { continue }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $target "bucket\$name.json")) -and
|
||||
-not (Test-Path -LiteralPath (Join-Path $target "$name.json"))) {
|
||||
Write-Warning "Für '$name' fehlt weiterhin ein gleichnamiges Manifest."
|
||||
}
|
||||
}
|
||||
Write-Host "Fertig: $($changes.Count) install.json-Dateien auf medelexis umgestellt."
|
||||
Write-Host "Paketnamen umgestellt: $($renames.Count)."
|
||||
Write-Host 'Von elexis/streichtech/medelexis bleibt nur medelexis bestehen. Prüfung: scoop status --local'
|
||||
|
||||
do {
|
||||
$autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant()
|
||||
} while ($autoUpdateAnswer -notin @('y', 'n'))
|
||||
|
||||
if ($autoUpdateAnswer -eq 'y') {
|
||||
$autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis'
|
||||
New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null
|
||||
$autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1'
|
||||
$downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid())
|
||||
try {
|
||||
$autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1"
|
||||
Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing
|
||||
$parseTokens = $null
|
||||
$parseErrors = $null
|
||||
$downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors)
|
||||
if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or
|
||||
'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) {
|
||||
throw 'Das heruntergeladene Auto-Update-Skript ist kein gueltiges Updateskript.'
|
||||
}
|
||||
Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force
|
||||
# Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet.
|
||||
$userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
$taskName = "Medelexis-Scoop-AutoUpdate-$userSid"
|
||||
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||
$arguments = '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -ScoopRoot "{1}"' -f $autoUpdateScript, $ScoopRoot
|
||||
$action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments
|
||||
$trigger = New-ScheduledTaskTrigger -Daily -At '03:00'
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable `
|
||||
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 4)
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
|
||||
-Principal $principal -Settings $settings -Force `
|
||||
-Description 'Scoop taeglich um 03:00 aktualisieren; verpasste Termine nachholen; elexis* auf hold setzen.' | Out-Null
|
||||
Write-Host "Scoop Auto-Update eingeschaltet: $taskName (taeglich 03:00, verpasste Termine nachholen)."
|
||||
Write-Host "Ausfuehrung verborgen unter $userId, sobald der Benutzer angemeldet ist; kein Aufwecken des PCs."
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath }
|
||||
}
|
||||
} else {
|
||||
Write-Host 'Auto-Update nicht eingerichtet. Eine eventuell bestehende Aufgabe bleibt unveraendert.'
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#requires -Version 5.1
|
||||
# Isolated behavioural tests: no real downloads, Scoop updates or scheduled tasks.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$projectRoot = Split-Path $PSScriptRoot -Parent
|
||||
$fixtureRoot = Join-Path $PSScriptRoot ('.auto-update-test-' + [guid]::NewGuid())
|
||||
$oldScoop = $env:SCOOP
|
||||
$oldPath = $env:PATH
|
||||
$oldFixture = $env:SCOOP_AUTO_UPDATE_TEST_ROOT
|
||||
$oldFailure = $env:SCOOP_AUTO_UPDATE_TEST_FAILURE
|
||||
|
||||
function Assert-True($Condition, [string] $Message) {
|
||||
if (-not $Condition) { throw $Message }
|
||||
}
|
||||
|
||||
# These stand-ins capture scheduling settings without changing Windows.
|
||||
function New-ScheduledTaskAction { param($Execute, $Argument) [pscustomobject]$PSBoundParameters }
|
||||
function New-ScheduledTaskTrigger { param([switch]$Daily, $At) [pscustomobject]$PSBoundParameters }
|
||||
function New-ScheduledTaskPrincipal { param($UserId, $LogonType, $RunLevel) [pscustomobject]$PSBoundParameters }
|
||||
function New-ScheduledTaskSettingsSet {
|
||||
param([switch]$StartWhenAvailable, [switch]$RunOnlyIfNetworkAvailable,
|
||||
[switch]$AllowStartIfOnBatteries, [switch]$DontStopIfGoingOnBatteries,
|
||||
$MultipleInstances, $ExecutionTimeLimit)
|
||||
[pscustomobject]$PSBoundParameters
|
||||
}
|
||||
function Register-ScheduledTask {
|
||||
param($TaskName, $Action, $Trigger, $Principal, $Settings, [switch]$Force, $Description)
|
||||
$global:AutoUpdateTestTask = [pscustomobject]$PSBoundParameters
|
||||
}
|
||||
function Read-Host {
|
||||
param($Prompt)
|
||||
$global:AutoUpdateTestAnswers.Dequeue()
|
||||
}
|
||||
function Invoke-WebRequest {
|
||||
param($Uri, $OutFile, [switch]$UseBasicParsing)
|
||||
Assert-True ($Uri -in @('https://example.test/scoop/raw/branch/master/scripts/auto-update.ps1',
|
||||
'https://gitea.medelexis.ch/public/scoop/raw/branch/master/scripts/auto-update.ps1')) 'Unexpected download URL.'
|
||||
if ($global:AutoUpdateTestInvalidDownload) {
|
||||
'This is not the setup script.' | Set-Content -LiteralPath $OutFile
|
||||
} else {
|
||||
@'
|
||||
param([string] $ScoopRoot)
|
||||
throw 'The downloaded runner must not execute during setup.'
|
||||
'@ | Set-Content -LiteralPath $OutFile
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
# Spaces in the installation path exercise the scheduled command's quoting.
|
||||
$scoopRoot = Join-Path $fixtureRoot 'scoop with spaces'
|
||||
$shims = Join-Path $scoopRoot 'shims'
|
||||
New-Item -ItemType Directory -Path $shims -Force | Out-Null
|
||||
$scriptPath = Join-Path $fixtureRoot 'auto-update.ps1'
|
||||
Copy-Item -LiteralPath (Join-Path $projectRoot 'scripts\auto-update.ps1') -Destination $scriptPath
|
||||
$env:SCOOP_AUTO_UPDATE_TEST_ROOT = $fixtureRoot
|
||||
$commandLog = Join-Path $fixtureRoot 'commands.log'
|
||||
@'
|
||||
param([Parameter(ValueFromRemainingArguments = $true)][string[]] $ScoopArguments)
|
||||
Add-Content -LiteralPath (Join-Path $env:SCOOP_AUTO_UPDATE_TEST_ROOT 'commands.log') -Value ($ScoopArguments -join ' ')
|
||||
if ($env:SCOOP_AUTO_UPDATE_TEST_FAILURE -eq ($ScoopArguments -join ' ')) { exit 1 }
|
||||
if ($ScoopArguments[0] -eq 'list') {
|
||||
[pscustomobject]@{ Name = 'elexis'; Info = '' }
|
||||
[pscustomobject]@{ Name = 'Elexis-test'; Info = 'Held package' }
|
||||
[pscustomobject]@{ Name = 'myelexis-cli'; Info = '' }
|
||||
[pscustomobject]@{ Name = 'git'; Info = '' }
|
||||
[pscustomobject]@{ Name = 'elexis-global'; Info = 'Global install' }
|
||||
[pscustomobject]@{ Name = 'elexis-broken'; Info = 'Install failed' }
|
||||
}
|
||||
exit 0
|
||||
'@ | Set-Content -LiteralPath (Join-Path $shims 'scoop.ps1') -Encoding UTF8
|
||||
|
||||
& $scriptPath -ScoopRoot $scoopRoot
|
||||
Assert-True ($LASTEXITCODE -eq 0) 'Expected successful update.'
|
||||
Assert-True (((Get-Content $commandLog) -join '|') -eq 'list|hold elexis|hold Elexis-test|update|update *') 'Expected only local elexis* held before updates.'
|
||||
Assert-True (Test-Path (Join-Path $fixtureRoot 'auto-update.log')) 'Expected update log.'
|
||||
|
||||
foreach ($failure in @('list', 'hold elexis', 'update', 'update *')) {
|
||||
Remove-Item -LiteralPath $commandLog
|
||||
$env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $failure
|
||||
# Expected errors are logged; do not let test ErrorActionPreference terminate the caller.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $scriptPath -ScoopRoot $scoopRoot 2>$null
|
||||
$result = $LASTEXITCODE
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Assert-True ($result -eq 1) "Expected failure status for $failure."
|
||||
$commands = @(Get-Content $commandLog)
|
||||
Assert-True ($commands[-1] -eq $failure) "Must abort immediately after $failure."
|
||||
}
|
||||
# Exercise the actual opt-in/download block without running the bucket migration.
|
||||
$tokens = $null
|
||||
$errors = $null
|
||||
$bucketAst = [Management.Automation.Language.Parser]::ParseFile((Join-Path $projectRoot 'scripts\update-bucket.ps1'), [ref]$tokens, [ref]$errors)
|
||||
Assert-True ($errors.Count -eq 0) 'Invalid bucket script syntax.'
|
||||
$optInBlock = [scriptblock]::Create((($bucketAst.EndBlock.Statements | Select-Object -Last 2 | ForEach-Object { $_.Extent.Text }) -join "`n"))
|
||||
$repository = 'https://example.test/scoop'
|
||||
Remove-Item -LiteralPath $commandLog
|
||||
$global:AutoUpdateTestAnswers = [Collections.Generic.Queue[string]]::new()
|
||||
$global:AutoUpdateTestAnswers.Enqueue('n')
|
||||
& $optInBlock
|
||||
$downloadDirectory = Join-Path $scoopRoot 'scripts\medelexis'
|
||||
Assert-True (-not (Test-Path $downloadDirectory)) 'Declining must not download or set up anything.'
|
||||
$global:AutoUpdateTestAnswers.Enqueue('invalid')
|
||||
$global:AutoUpdateTestAnswers.Enqueue('Y')
|
||||
& $optInBlock
|
||||
$downloadedPath = Join-Path $downloadDirectory 'auto-update.ps1'
|
||||
Assert-True (Test-Path $downloadedPath) 'Expected downloaded runner.'
|
||||
$task = $global:AutoUpdateTestTask
|
||||
Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00') 'Expected daily 03:00 schedule.'
|
||||
Assert-True ($task.Settings.StartWhenAvailable -and $task.Settings.RunOnlyIfNetworkAvailable) 'Expected catch-up and network settings.'
|
||||
Assert-True ($task.Settings.MultipleInstances -eq 'IgnoreNew') 'Expected overlap protection.'
|
||||
Assert-True ($task.Principal.LogonType -eq 'Interactive' -and $task.Principal.RunLevel -eq 'Limited') 'Expected normal user session.'
|
||||
Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-NonInteractive')) 'Expected background execution.'
|
||||
Assert-True ($task.Action.Argument.Contains('-File "' + $downloadedPath + '"') -and $task.Action.Argument.Contains('-ScoopRoot "' + $scoopRoot + '"')) 'Expected quoted paths pointing to the downloaded runner.'
|
||||
Assert-True (-not (Test-Path $commandLog)) 'Setup must not execute Scoop.'
|
||||
$taskName = $task.TaskName
|
||||
$global:AutoUpdateTestAnswers.Enqueue('y')
|
||||
& $optInBlock
|
||||
Assert-True ($global:AutoUpdateTestTask.TaskName -eq $taskName -and $global:AutoUpdateTestTask.Force) 'Repeated setup must replace the same task.'
|
||||
$originalDownload = [IO.File]::ReadAllText($downloadedPath)
|
||||
Remove-Variable AutoUpdateTestTask -Scope Global
|
||||
$global:AutoUpdateTestInvalidDownload = $true
|
||||
$global:AutoUpdateTestAnswers.Enqueue('y')
|
||||
$rejected = $false
|
||||
try { & $optInBlock } catch { $rejected = $true }
|
||||
Assert-True $rejected 'Invalid download must be rejected.'
|
||||
Assert-True ([IO.File]::ReadAllText($downloadedPath) -ceq $originalDownload) 'Invalid download must preserve the existing script.'
|
||||
Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Invalid download must not register a task.'
|
||||
Assert-True (@(Get-ChildItem $downloadDirectory -Filter '*.download').Count -eq 0) 'Temporary downloads must be removed.'
|
||||
# The additional standalone installer must offer the same setup without migration.
|
||||
$standaloneSetup = Join-Path $projectRoot 'scripts\setup-auto-update.ps1'
|
||||
$global:AutoUpdateTestAnswers.Enqueue('n')
|
||||
& $standaloneSetup -ScoopRoot $scoopRoot
|
||||
Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Standalone n must not register a task.'
|
||||
$global:AutoUpdateTestInvalidDownload = $false
|
||||
$global:AutoUpdateTestAnswers.Enqueue('y')
|
||||
& $standaloneSetup -ScoopRoot $scoopRoot
|
||||
$task = $global:AutoUpdateTestTask
|
||||
Assert-True ($task.TaskName -eq $taskName -and $task.Force) 'Standalone setup must use the same task.'
|
||||
Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00' -and $task.Settings.StartWhenAvailable) 'Standalone setup must schedule daily 03:00 with catch-up.'
|
||||
Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-File "' + $downloadedPath + '"')) 'Standalone task must invoke the downloaded runner hidden.'
|
||||
Assert-True (-not (Test-Path $commandLog)) 'Standalone setup must not run Scoop or migrations.'
|
||||
Write-Host 'PASS: schedule, repeat setup, hold filtering, command order, logging, failures, opt-in and download setup.'
|
||||
} finally {
|
||||
$env:SCOOP = $oldScoop
|
||||
$env:PATH = $oldPath
|
||||
$env:SCOOP_AUTO_UPDATE_TEST_ROOT = $oldFixture
|
||||
$env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $oldFailure
|
||||
Remove-Variable AutoUpdateTestTask, AutoUpdateTestAnswers, AutoUpdateTestInvalidDownload -Scope Global -ErrorAction SilentlyContinue
|
||||
# Delete only this test's unique directory, after checking its absolute parent.
|
||||
if ((Test-Path -LiteralPath $fixtureRoot) -and
|
||||
[IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($fixtureRoot)) -eq [IO.Path]::GetFullPath($PSScriptRoot) -and
|
||||
[IO.Path]::GetFileName($fixtureRoot).StartsWith('.auto-update-test-')) {
|
||||
Remove-Item -LiteralPath $fixtureRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user