From dccac65ea7abdef73d5d0ffd3273e53e6ceb6199 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 9 Jun 2026 16:33:28 +0100 Subject: [PATCH] Enforce coherent Idunn swarm deployments --- docs/idunn.md | 12 ++++ scripts/deploy-yggdrasil-heimdall.cmd | 2 + scripts/deploy-yggdrasil-repixelizer.cmd | 2 + scripts/deploy-yggdrasil-source-app.ps1 | 84 +++++++++++++++++++++++ scripts/deploy-yggdrasil-streampixels.cmd | 2 + scripts/health-yggdrasil-heimdall.cmd | 2 + scripts/health-yggdrasil-repixelizer.cmd | 2 + scripts/health-yggdrasil-source-app.ps1 | 43 ++++++++++++ scripts/health-yggdrasil-streampixels.cmd | 2 + scripts/idunn-deployment-targets.ps1 | 33 +++++---- scripts/start-idunn-local.ps1 | 24 +++++++ src/Idunn/README.md | 15 ++-- 12 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 scripts/deploy-yggdrasil-heimdall.cmd create mode 100644 scripts/deploy-yggdrasil-repixelizer.cmd create mode 100644 scripts/deploy-yggdrasil-source-app.ps1 create mode 100644 scripts/deploy-yggdrasil-streampixels.cmd create mode 100644 scripts/health-yggdrasil-heimdall.cmd create mode 100644 scripts/health-yggdrasil-repixelizer.cmd create mode 100644 scripts/health-yggdrasil-source-app.ps1 create mode 100644 scripts/health-yggdrasil-streampixels.cmd diff --git a/docs/idunn.md b/docs/idunn.md index 9774ff5..8b62afc 100644 --- a/docs/idunn.md +++ b/docs/idunn.md @@ -53,6 +53,11 @@ Idunn now shares Odin's Rust body: through `health-muninn.cmd` and `restart-muninn.cmd`, and Nightwing Gjallar through `health-nightwing-gjallar.cmd`, `deploy-nightwing-gjallar.cmd`, and `restart-nightwing-gjallar.cmd`. +- `scripts/deploy-yggdrasil-source-app.ps1` and + `scripts/health-yggdrasil-source-app.ps1` are the generic Yggdrasil source + artifact lane. They package committed local `HEAD` with `git archive`, run + the existing ops-owned deploy/check scripts on Yggdrasil, and stamp a remote + `gamecult.idunn.deployment_manifest.v1` only after the remote check passes. - `scripts/idunn-deployment-targets.ps1` is the current swarm deployment target catalog. Every known deployable target is either `enforced`, `blocked`, `external-owned`, or `not-runtime` with an explicit reason. @@ -137,6 +142,13 @@ idunn.operator_alarm.v1 deploy command must remain `blocked` or `external-owned` with the missing authority named until a wrapper can produce a deployment manifest and route through Idunn's typed deployment request/result path. +- Yggdrasil Heimdall, repixelizer, and StreamPixels are enforced through the + generic source artifact lane and their existing `gamecult-ops` runbooks. + Bifrost is explicitly blocked as of 2026-06-09: committed Bifrost `HEAD` + expects `UserAccounts.HeimdallAccountId`, while Yggdrasil's production + database lacks that column and EF reports no pending migration. Idunn must + not claim Bifrost deployment freshness until Bifrost owns that schema + migration path. ## First Runtime Direction diff --git a/scripts/deploy-yggdrasil-heimdall.cmd b/scripts/deploy-yggdrasil-heimdall.cmd new file mode 100644 index 0000000..cd9b241 --- /dev/null +++ b/scripts/deploy-yggdrasil-heimdall.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\deploy-yggdrasil-source-app.ps1 -AppId yggdrasil-heimdall -RepoRoot E:\Projects\Heimdall -AppUser heimdall -RemoteAppHome /srv/heimdall -RemoteTarballName heimdall-source.tar -DeployScript E:\Projects\gamecult-ops\scripts\deploy-heimdall.sh -CheckScript E:\Projects\gamecult-ops\scripts\check-heimdall.sh diff --git a/scripts/deploy-yggdrasil-repixelizer.cmd b/scripts/deploy-yggdrasil-repixelizer.cmd new file mode 100644 index 0000000..9b96737 --- /dev/null +++ b/scripts/deploy-yggdrasil-repixelizer.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\deploy-yggdrasil-source-app.ps1 -AppId yggdrasil-repixelizer -RepoRoot E:\Projects\repixelizer -AppUser repixelizer -RemoteAppHome /srv/repixelizer -RemoteTarballName repixelizer-source.tar -DeployScript E:\Projects\gamecult-ops\scripts\deploy-repixelizer-gui.sh -CheckScript E:\Projects\gamecult-ops\scripts\check-repixelizer-gui.sh diff --git a/scripts/deploy-yggdrasil-source-app.ps1 b/scripts/deploy-yggdrasil-source-app.ps1 new file mode 100644 index 0000000..54819d9 --- /dev/null +++ b/scripts/deploy-yggdrasil-source-app.ps1 @@ -0,0 +1,84 @@ +param( + [Parameter(Mandatory = $true)] [string] $AppId, + [Parameter(Mandatory = $true)] [string] $RepoRoot, + [Parameter(Mandatory = $true)] [string] $AppUser, + [Parameter(Mandatory = $true)] [string] $RemoteAppHome, + [Parameter(Mandatory = $true)] [string] $RemoteTarballName, + [Parameter(Mandatory = $true)] [string] $DeployScript, + [Parameter(Mandatory = $true)] [string] $CheckScript, + [string] $SshTarget = "yggdrasil" +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path -LiteralPath $RepoRoot)) { + throw "Repo root not found: $RepoRoot" +} +if (-not (Test-Path -LiteralPath $DeployScript)) { + throw "Deploy script not found: $DeployScript" +} +if (-not (Test-Path -LiteralPath $CheckScript)) { + throw "Check script not found: $CheckScript" +} + +$commit = (git -C $RepoRoot rev-parse HEAD).Trim() +if ([string]::IsNullOrWhiteSpace($commit)) { + throw "Could not determine git revision for $RepoRoot" +} + +$scratch = Join-Path "E:\Projects\Odin\scratch\idunn-deploy" $AppId +$tarPath = Join-Path $scratch $RemoteTarballName +$manifestPath = Join-Path $scratch "deployment-manifest.txt" +New-Item -ItemType Directory -Force -Path $scratch | Out-Null + +git -C $RepoRoot archive --format=tar --output=$tarPath HEAD +if ($LASTEXITCODE -ne 0) { + throw "git archive failed for $RepoRoot" +} + +$hash = (Get-FileHash $tarPath -Algorithm SHA256).Hash.ToLowerInvariant() +$deployedAt = [DateTimeOffset]::UtcNow.ToString("O") +@( + "schema=gamecult.idunn.deployment_manifest.v1" + "appId=$AppId" + "repoRoot=$RepoRoot" + "gitCommit=$commit" + "artifact=$RemoteTarballName" + "sha256=$hash" + "deployedAtUtc=$deployedAt" +) | Set-Content -Encoding ASCII -LiteralPath $manifestPath + +$remoteDeploy = "/home/gamecultadmin/$([IO.Path]::GetFileName($DeployScript))" +$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))" +$remoteTarball = "/home/gamecultadmin/$RemoteTarballName" +$remoteManifest = "/home/gamecultadmin/$AppId-deployment-manifest.txt" +$targetTarball = "$RemoteAppHome/$RemoteTarballName" +$targetManifest = "$RemoteAppHome/deployment-manifest.txt" +$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10", $SshTarget) +$scpArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10") + +scp.exe @scpArgs $tarPath "${SshTarget}:$remoteTarball" +if ($LASTEXITCODE -ne 0) { throw "scp source tarball failed for $AppId" } +scp.exe @scpArgs $DeployScript "${SshTarget}:$remoteDeploy" +if ($LASTEXITCODE -ne 0) { throw "scp deploy script failed for $AppId" } +scp.exe @scpArgs $CheckScript "${SshTarget}:$remoteCheck" +if ($LASTEXITCODE -ne 0) { throw "scp check script failed for $AppId" } +scp.exe @scpArgs $manifestPath "${SshTarget}:$remoteManifest" +if ($LASTEXITCODE -ne 0) { throw "scp manifest failed for $AppId" } + +$remoteCommand = @( + "sudo -n install -d -o '$AppUser' -g '$AppUser' '$RemoteAppHome'", + "sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteTarball' '$targetTarball'", + "chmod +x '$remoteDeploy' '$remoteCheck'", + "sudo -n bash '$remoteDeploy'", + "check_ok=0; for i in `$(seq 1 30); do if sudo -n bash '$remoteCheck'; then check_ok=1; break; fi; sleep 2; done; if [ `$check_ok -ne 1 ]; then sudo -n bash '$remoteCheck'; fi", + "sudo -n install -m 644 '$remoteManifest' '$targetManifest'" +) -join " && " + +ssh.exe @sshArgs $remoteCommand +if ($LASTEXITCODE -ne 0) { + throw "remote deploy/check failed for $AppId" +} + +Write-Host "$AppId deployed to Yggdrasil at $commit." +Write-Host " artifactSha256=$hash" diff --git a/scripts/deploy-yggdrasil-streampixels.cmd b/scripts/deploy-yggdrasil-streampixels.cmd new file mode 100644 index 0000000..54948a1 --- /dev/null +++ b/scripts/deploy-yggdrasil-streampixels.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\deploy-yggdrasil-source-app.ps1 -AppId yggdrasil-streampixels -RepoRoot E:\Projects\StreamPixels -AppUser streampixels -RemoteAppHome /srv/streampixels -RemoteTarballName streampixels-source.tar -DeployScript E:\Projects\gamecult-ops\scripts\deploy-streampixels-preview.sh -CheckScript E:\Projects\gamecult-ops\scripts\check-streampixels-preview.sh diff --git a/scripts/health-yggdrasil-heimdall.cmd b/scripts/health-yggdrasil-heimdall.cmd new file mode 100644 index 0000000..63b239a --- /dev/null +++ b/scripts/health-yggdrasil-heimdall.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-yggdrasil-source-app.ps1 -AppId yggdrasil-heimdall -RepoRoot E:\Projects\Heimdall -RemoteAppHome /srv/heimdall -CheckScript E:\Projects\gamecult-ops\scripts\check-heimdall.sh diff --git a/scripts/health-yggdrasil-repixelizer.cmd b/scripts/health-yggdrasil-repixelizer.cmd new file mode 100644 index 0000000..b1080b6 --- /dev/null +++ b/scripts/health-yggdrasil-repixelizer.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-yggdrasil-source-app.ps1 -AppId yggdrasil-repixelizer -RepoRoot E:\Projects\repixelizer -RemoteAppHome /srv/repixelizer -CheckScript E:\Projects\gamecult-ops\scripts\check-repixelizer-gui.sh diff --git a/scripts/health-yggdrasil-source-app.ps1 b/scripts/health-yggdrasil-source-app.ps1 new file mode 100644 index 0000000..54df0c2 --- /dev/null +++ b/scripts/health-yggdrasil-source-app.ps1 @@ -0,0 +1,43 @@ +param( + [Parameter(Mandatory = $true)] [string] $AppId, + [Parameter(Mandatory = $true)] [string] $RepoRoot, + [Parameter(Mandatory = $true)] [string] $RemoteAppHome, + [Parameter(Mandatory = $true)] [string] $CheckScript, + [string] $SshTarget = "yggdrasil" +) + +$ErrorActionPreference = "Stop" + +$localCommit = (git -C $RepoRoot rev-parse HEAD).Trim() +if ([string]::IsNullOrWhiteSpace($localCommit)) { + throw "Could not determine git revision for $RepoRoot" +} + +$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))" +$scpArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10") +$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10", $SshTarget) +scp.exe @scpArgs $CheckScript "${SshTarget}:$remoteCheck" | Out-Null +if ($LASTEXITCODE -ne 0) { throw "scp check script failed for $AppId" } + +$manifest = ssh.exe @sshArgs "cat '$RemoteAppHome/deployment-manifest.txt' 2>/dev/null" +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($manifest)) { + throw "$AppId deploy manifest is missing on Yggdrasil." +} + +$remoteCommit = "" +foreach ($line in $manifest) { + if ($line -like "gitCommit=*") { + $remoteCommit = $line.Substring("gitCommit=".Length).Trim() + } +} + +if ($remoteCommit -ne $localCommit) { + throw "$AppId deploy is stale. remote=$remoteCommit local=$localCommit" +} + +ssh.exe @sshArgs "chmod +x '$remoteCheck' && sudo -n bash '$remoteCheck'" +if ($LASTEXITCODE -ne 0) { + throw "$AppId remote health check failed." +} + +Write-Host "$AppId is active and deployed at $localCommit." diff --git a/scripts/health-yggdrasil-streampixels.cmd b/scripts/health-yggdrasil-streampixels.cmd new file mode 100644 index 0000000..868968e --- /dev/null +++ b/scripts/health-yggdrasil-streampixels.cmd @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-yggdrasil-source-app.ps1 -AppId yggdrasil-streampixels -RepoRoot E:\Projects\StreamPixels -RemoteAppHome /srv/streampixels -CheckScript E:\Projects\gamecult-ops\scripts\check-streampixels-preview.sh diff --git a/scripts/idunn-deployment-targets.ps1 b/scripts/idunn-deployment-targets.ps1 index 7f7eac9..db5a0ac 100644 --- a/scripts/idunn-deployment-targets.ps1 +++ b/scripts/idunn-deployment-targets.ps1 @@ -1,5 +1,4 @@ $repoRoot = Split-Path -Parent $PSScriptRoot -$opsRoot = "E:\Projects\gamecult-ops" $IdunnDeploymentTargets = @( [pscustomobject]@{ @@ -19,12 +18,12 @@ $IdunnDeploymentTargets = @( Repo = "Bifrost" LocalPath = "E:\Projects\Bifrost" Host = "yggdrasil" - Service = "bifrost.service/bifrost-web" + Service = "bifrost.service" Status = "blocked" - Health = "$opsRoot\scripts\check-bifrost-alpha.sh" - Deploy = "$opsRoot\scripts\deploy-bifrost-container.ps1" + Health = $null + Deploy = $null Restart = $null - Reason = "Deploy authority exists in gamecult-ops, but desired image digest/tag is not yet published as an Idunn deployment target record." + Reason = "Committed Bifrost HEAD currently expects UserAccounts.HeimdallAccountId, but Yggdrasil's production database lacks that column and EF reports no pending migration. Idunn must not enforce Bifrost freshness until the Bifrost schema migration path is fixed." }, [pscustomobject]@{ Id = "yggdrasil-streampixels" @@ -32,11 +31,11 @@ $IdunnDeploymentTargets = @( LocalPath = "E:\Projects\StreamPixels" Host = "yggdrasil" Service = "streampixels-service/streampixels-web" - Status = "blocked" - Health = "$opsRoot\scripts\check-streampixels-preview.sh" - Deploy = "$opsRoot\scripts\deploy-streampixels-preview.sh" + Status = "enforced" + Health = "$repoRoot\scripts\health-yggdrasil-streampixels.cmd" + Deploy = "$repoRoot\scripts\deploy-yggdrasil-streampixels.cmd" Restart = $null - Reason = "Deploy script is remote-root/runbook shaped and needs a committed artifact manifest plus Idunn-safe wrapper before automatic execution." + Reason = "Idunn packages committed local StreamPixels HEAD, runs the existing ops deploy/check scripts, and verifies the remote manifest." }, [pscustomobject]@{ Id = "yggdrasil-heimdall" @@ -44,11 +43,11 @@ $IdunnDeploymentTargets = @( LocalPath = "E:\Projects\Heimdall" Host = "yggdrasil" Service = "heimdall.service" - Status = "blocked" - Health = "$opsRoot\scripts\check-heimdall.sh" - Deploy = "$opsRoot\scripts\deploy-heimdall.sh" + Status = "enforced" + Health = "$repoRoot\scripts\health-yggdrasil-heimdall.cmd" + Deploy = "$repoRoot\scripts\deploy-yggdrasil-heimdall.cmd" Restart = $null - Reason = "Deploy script uses a workstation-built source tarball; Idunn needs a manifest-producing wrapper before automatic execution." + Reason = "Idunn packages committed local Heimdall HEAD, runs the existing ops deploy/check scripts, and verifies the remote manifest." }, [pscustomobject]@{ Id = "yggdrasil-repixelizer" @@ -56,11 +55,11 @@ $IdunnDeploymentTargets = @( LocalPath = "E:\Projects\repixelizer" Host = "yggdrasil" Service = "repixelizer-gui.service" - Status = "blocked" - Health = "$opsRoot\scripts\check-repixelizer-gui.sh" - Deploy = "$opsRoot\scripts\deploy-repixelizer-gui.sh" + Status = "enforced" + Health = "$repoRoot\scripts\health-yggdrasil-repixelizer.cmd" + Deploy = "$repoRoot\scripts\deploy-yggdrasil-repixelizer.cmd" Restart = $null - Reason = "Deploy script uses a committed source tarball and Python environment mutation; Idunn needs an artifact manifest wrapper before automatic execution." + Reason = "Idunn packages committed local repixelizer HEAD, runs the existing ops deploy/check scripts, and verifies the remote manifest." }, [pscustomobject]@{ Id = "github-pages-gamecult-site" diff --git a/scripts/start-idunn-local.ps1 b/scripts/start-idunn-local.ps1 index cb2c55f..ce595bd 100644 --- a/scripts/start-idunn-local.ps1 +++ b/scripts/start-idunn-local.ps1 @@ -69,6 +69,30 @@ $watchdogs = @( Health = "$repoRoot\scripts\health-idunn-swarm-deployment-coverage.cmd" Restart = $null }, + [pscustomobject]@{ + Id = "yggdrasil-heimdall" + Name = "Yggdrasil Heimdall" + Verse = "yggdrasil.local" + Health = "$repoRoot\scripts\health-yggdrasil-heimdall.cmd" + Deploy = "$repoRoot\scripts\deploy-yggdrasil-heimdall.cmd" + IntervalSeconds = 300 + }, + [pscustomobject]@{ + Id = "yggdrasil-repixelizer" + Name = "Yggdrasil Repixelizer" + Verse = "yggdrasil.local" + Health = "$repoRoot\scripts\health-yggdrasil-repixelizer.cmd" + Deploy = "$repoRoot\scripts\deploy-yggdrasil-repixelizer.cmd" + IntervalSeconds = 300 + }, + [pscustomobject]@{ + Id = "yggdrasil-streampixels" + Name = "Yggdrasil StreamPixels" + Verse = "yggdrasil.local" + Health = "$repoRoot\scripts\health-yggdrasil-streampixels.cmd" + Deploy = "$repoRoot\scripts\deploy-yggdrasil-streampixels.cmd" + IntervalSeconds = 300 + }, [pscustomobject]@{ Id = "nightwing-gjallar" Name = "Nightwing Gjallar framebuffer compositor" diff --git a/src/Idunn/README.md b/src/Idunn/README.md index a6722f0..58a8f7e 100644 --- a/src/Idunn/README.md +++ b/src/Idunn/README.md @@ -75,9 +75,10 @@ Install the local Starfire boot watchdogs: .\scripts\install-idunn-startup.ps1 ``` -That task starts Idunn loops for Odin, Stonks, and the Nightwing display -services at user logon. The current Mimir dashboard is observed but not -restarted until its restart authority is named. +That task starts Idunn loops for Odin, Stonks, the enforced Yggdrasil source +artifact targets, and the Nightwing display services at user logon. The current +Mimir dashboard is observed but not restarted until its restart authority is +named. It also starts `idunn-swarm-deployment-coverage`, which watches the deployment target catalog in `scripts/idunn-deployment-targets.ps1`. A repo/service is not @@ -129,9 +130,11 @@ For the repo swarm, deployment ownership begins with catalog coverage. Use: .\scripts\health-idunn-swarm-deployment-coverage.cmd ``` -The first fully enforced target is Nightwing Gjallar. Other targets remain -blocked or external-owned until their deployment commands can produce manifests -and run safely through Idunn. +The enforced targets are Nightwing Gjallar plus the Yggdrasil source artifact +lanes whose ops runbooks can produce and verify deployment manifests. Bifrost +is blocked until its production database migration path is coherent; mobile +device installs remain blocked at their physical approval boundaries; GitHub +Pages remains external-owned. ## Typed Records