Enforce coherent Idunn swarm deployments

This commit is contained in:
Meta Vens 2026-06-09 16:33:28 +01:00
commit dccac65ea7
12 changed files with 200 additions and 23 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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