Skip to content

fix(viewer): resolve workshop mod roots (#110) #177

fix(viewer): resolve workshop mod roots (#110)

fix(viewer): resolve workshop mod roots (#110) #177

Workflow file for this run

name: release
on:
workflow_dispatch:
push:
branches:
- main
paths:
- ".github/workflows/release.yml"
- "Directory.Build.props"
- "Magnetar.Protocol/**"
- "Quasar/**"
- "Quasar.Agent/**"
- "Quasar.Bootstrap/**"
- "Quasar.sln"
- "install.sh"
- "uninstall.sh"
- "scripts/install.ps1"
- "scripts/package-linux-release.sh"
- "scripts/package-windows-release.ps1"
- "scripts/readme-install-linux.md"
- "scripts/readme-install-windows.md"
- "scripts/uninstall.ps1"
tags:
- "v*"
pull_request:
branches:
- main
paths:
- ".github/workflows/release.yml"
- "Directory.Build.props"
- "Magnetar.Protocol/**"
- "Quasar/**"
- "Quasar.Agent/**"
- "Quasar.Bootstrap/**"
- "Quasar.sln"
- "install.sh"
- "uninstall.sh"
- "scripts/install.ps1"
- "scripts/package-linux-release.sh"
- "scripts/package-windows-release.ps1"
- "scripts/readme-install-linux.md"
- "scripts/readme-install-windows.md"
- "scripts/uninstall.ps1"
permissions:
contents: write
env:
SE_DS_APPID: "298740"
jobs:
# Resolve the release identity once so the Linux and Windows builds stamp the
# same version and all archives land under a single GitHub release/tag.
metadata:
# Draft PRs do not need release artifacts; skipping here cascades to every
# downstream job via `needs`, so no build infra is spent until the PR is
# marked ready for review. Non-PR events (push, tag, workflow_dispatch)
# always run.
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
runs-on: ubuntu-latest
outputs:
version: ${{ steps.release.outputs.version }}
tag: ${{ steps.release.outputs.tag }}
name: ${{ steps.release.outputs.name }}
draft: ${{ steps.release.outputs.draft }}
prerelease: ${{ steps.release.outputs.prerelease }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve Quasar release metadata
id: release
env:
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
run: |
set -euo pipefail
# Single source of truth for the base version: Directory.Build.props.
# Non-tag builds append a channel/run suffix to this base; tag pushes use
# the tag name verbatim.
base="$(sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' Directory.Build.props | head -n1)"
if [[ -z "$base" ]]; then
echo "Could not read <Version> from Directory.Build.props." >&2
exit 1
fi
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
version="${GITHUB_REF_NAME#v}"
tag="v${version}"
draft="false"
prerelease="false"
elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
version="${base}-manual.${GITHUB_RUN_NUMBER}"
tag="v${version}"
draft="true"
prerelease="true"
elif [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
# The build number (4th component) must reset to zero whenever the
# base version (the first three components in Directory.Build.props)
# changes. GITHUB_RUN_NUMBER cannot do this: it increments for the
# lifetime of the workflow regardless of version bumps. Instead derive
# the build number from git history by counting the commits since
# <Version> was last set to its current value. The commit that bumped
# the version yields build 0; each later commit increments by one.
bump_commit=""
while read -r sha; do
v="$(git show "$sha:Directory.Build.props" 2>/dev/null \
| sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' | head -n1)"
if [[ "$v" != "$base" ]]; then
break
fi
bump_commit="$sha"
done < <(git log --format=%H -- Directory.Build.props)
if [[ -n "$bump_commit" ]]; then
build="$(git rev-list --count "${bump_commit}..HEAD")"
else
build="$(git rev-list --count HEAD)"
fi
version="${base}.${build}"
tag="v${version}"
draft="false"
prerelease="false"
elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
if [[ -z "$PR_NUMBER" ]]; then
echo "Pull request number is not available." >&2
exit 1
fi
version="${base}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}"
tag="pr-${PR_NUMBER}/v${version}"
draft="true"
prerelease="true"
else
version="$(git rev-parse --short HEAD)"
tag="$version"
draft="true"
prerelease="true"
fi
# The release is always named "Quasar v<version>" with no extra suffix.
name="Quasar v${version}"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "name=$name" >> "$GITHUB_OUTPUT"
echo "draft=$draft" >> "$GITHUB_OUTPUT"
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
echo "Resolved release: tag=$tag version=$version draft=$draft prerelease=$prerelease"
build-linux:
needs: metadata
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.metadata.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: Install packaging dependencies
run: |
sudo dpkg --add-architecture i386
sudo add-apt-repository multiverse
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl lib32gcc-s1 p7zip-full
- name: Probe Dedicated Server build id
id: dsbuild
run: |
set -euo pipefail
mkdir -p steamcmd
curl -fsSL https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \
| tar -xz -C steamcmd
buildid=""
for attempt in 1 2 3 4 5; do
out="$(./steamcmd/steamcmd.sh +login anonymous +app_info_update 1 \
+app_info_print "$SE_DS_APPID" +quit 2>/dev/null || true)"
buildid="$(printf '%s' "$out" | tr -d '\r' | awk '
/"branches"/ { in_branches = 1 }
in_branches && /"public"/ { in_public = 1 }
in_public && /"buildid"/ { gsub(/[^0-9]/, "", $2); print $2; exit }
')"
[ -n "$buildid" ] && break
echo "Dedicated Server build id probe attempt $attempt empty; retrying..."
sleep 5
done
if [ -z "$buildid" ]; then
echo "WARNING: could not determine DS build id; DS cache will miss this run." >&2
buildid="norev-${{ github.run_id }}"
fi
echo "Dedicated Server public build id: $buildid"
echo "buildid=$buildid" >> "$GITHUB_OUTPUT"
- name: Cache Dedicated Server libraries
id: dscache
uses: actions/cache@v4
with:
path: ds64
key: ds64-linux-${{ steps.dsbuild.outputs.buildid }}
- name: Install Space Engineers Dedicated Server references
if: steps.dscache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
mkdir -p dsfull
./steamcmd/steamcmd.sh +quit || true
ok=0
for attempt in 1 2 3 4 5; do
echo "SteamCMD install attempt $attempt/5"
./steamcmd/steamcmd.sh \
+@sSteamCmdForcePlatformType windows \
+force_install_dir "$PWD/dsfull" \
+login anonymous \
+app_update "$SE_DS_APPID" validate \
+quit || true
if [ -f "$PWD/dsfull/DedicatedServer64/SpaceEngineersDedicated.exe" ]; then
ok=1
break
fi
echo "SteamCMD attempt $attempt did not complete the DS install; retrying..."
sleep 5
done
if [ "$ok" != 1 ]; then
echo "ERROR: SteamCMD failed to install Space Engineers Dedicated Server references." >&2
exit 1
fi
mkdir -p ds64
cp -a "$PWD/dsfull/DedicatedServer64/." ds64/
rm -rf dsfull
echo "Cached DS library set:"; du -sh ds64
- name: Install Magnetar PluginSdk reference
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p "$HOME/.local/share/Magnetar"
MAGNETAR_URL="$(python3 - <<'PY'
import fnmatch
import json
import os
import urllib.request
# Authenticate the API call: unauthenticated requests to api.github.com are
# capped at 60/hour per runner IP and intermittently fail with HTTP 403, so
# use the workflow token (matches the build-windows gh api call).
headers = {"User-Agent": "Quasar", "Accept": "application/vnd.github+json"}
token = os.environ.get("GH_TOKEN", "")
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(
"https://api.github.com/repos/CometWorks/magnetar/releases/latest",
headers=headers,
)
with urllib.request.urlopen(req) as response:
release = json.load(response)
if release.get("draft") or release.get("prerelease"):
raise SystemExit("latest Magnetar release is not a full release")
matches = [
asset for asset in release.get("assets", [])
if fnmatch.fnmatchcase(asset.get("name", ""), "MagnetarForLinux-*.7z")
]
if len(matches) != 1:
names = ", ".join(asset.get("name", "") for asset in release.get("assets", []))
raise SystemExit(f"expected one MagnetarForLinux-*.7z asset, found {len(matches)}: {names}")
print(matches[0]["browser_download_url"])
PY
)"
curl -fL "$MAGNETAR_URL" -o /tmp/magnetar.7z
7z x /tmp/magnetar.7z "-o$HOME/.local/share/Magnetar" -y
PLUGIN_SDK="$(find "$HOME/.local/share/Magnetar" -path "*/Bin/PluginSdk.dll" -print -quit)"
if [ -z "$PLUGIN_SDK" ]; then
echo "PluginSdk.dll not found in Magnetar archive" >&2
exit 1
fi
echo "MagnetarBin=$(dirname "$PLUGIN_SDK")" >> "$GITHUB_ENV"
- name: Package Linux release
env:
DS64: ${{ github.workspace }}/ds64
run: scripts/package-linux-release.sh
- name: Verify Linux installer archive root
run: |
set -euo pipefail
roots="$(tar -tzf artifacts/linux/quasar-installer-linux.tar.gz | awk -F/ 'NF { print $1 }' | sort -u)"
if [ "$roots" != "Quasar" ]; then
echo "ERROR: expected quasar-installer-linux.tar.gz root directory to be Quasar; found: $roots" >&2
exit 1
fi
- name: Upload Linux archives
uses: actions/upload-artifact@v4
with:
name: quasar-linux
path: |
artifacts/linux/quasar-web-linux-x64.tar.gz
artifacts/linux/quasar-installer-linux.tar.gz
build-windows:
needs: metadata
runs-on: windows-latest
env:
VERSION: ${{ needs.metadata.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: Probe Dedicated Server build id
id: dsbuild
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
New-Item -ItemType Directory -Force -Path steamcmd | Out-Null
Invoke-WebRequest -Uri 'https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip' -OutFile steamcmd.zip
Expand-Archive -Path steamcmd.zip -DestinationPath steamcmd -Force
$steamcmd = Join-Path (Resolve-Path steamcmd).Path 'steamcmd.exe'
$buildid = ''
foreach ($attempt in 1..5) {
$out = & $steamcmd +login anonymous +app_info_update 1 +app_info_print $env:SE_DS_APPID +quit 2>$null | Out-String
$inBranches = $false
$inPublic = $false
foreach ($line in ($out -split "`n")) {
if ($line -match '"branches"') { $inBranches = $true }
if ($inBranches -and $line -match '"public"') { $inPublic = $true }
if ($inPublic -and $line -match '"buildid"\s+"(\d+)"') { $buildid = $Matches[1]; break }
}
if ($buildid) { break }
Write-Host "Dedicated Server build id probe attempt $attempt empty; retrying..."
Start-Sleep -Seconds 5
}
if (-not $buildid) {
Write-Warning "could not determine DS build id; DS cache will miss this run."
$buildid = "norev-$env:GITHUB_RUN_ID"
}
Write-Host "Dedicated Server public build id: $buildid"
"buildid=$buildid" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
# steamcmd self-updates on first run and returns a non-zero exit code even
# on success; the GitHub pwsh shell exits the step with $LASTEXITCODE, so
# reset it explicitly now that we have the build id.
exit 0
- name: Cache Dedicated Server libraries
id: dscache
uses: actions/cache@v4
with:
path: ds64
key: ds64-windows-${{ steps.dsbuild.outputs.buildid }}
- name: Install Space Engineers Dedicated Server references
if: steps.dscache.outputs.cache-hit != 'true'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
$steamcmd = Join-Path (Resolve-Path steamcmd).Path 'steamcmd.exe'
New-Item -ItemType Directory -Force -Path dsfull | Out-Null
$dsfull = (Resolve-Path dsfull).Path
& $steamcmd +quit 2>$null | Out-Null
$ok = $false
foreach ($attempt in 1..5) {
Write-Host "SteamCMD install attempt $attempt/5"
# Native Windows depot; no +@sSteamCmdForcePlatformType override needed.
& $steamcmd +force_install_dir $dsfull +login anonymous +app_update $env:SE_DS_APPID validate +quit
if (Test-Path (Join-Path $dsfull 'DedicatedServer64\SpaceEngineersDedicated.exe')) { $ok = $true; break }
Write-Host "SteamCMD attempt $attempt did not complete the DS install; retrying..."
Start-Sleep -Seconds 5
}
if (-not $ok) {
Write-Error "SteamCMD failed to install Space Engineers Dedicated Server references."
exit 1
}
New-Item -ItemType Directory -Force -Path ds64 | Out-Null
Copy-Item -Path (Join-Path $dsfull 'DedicatedServer64\*') -Destination ds64 -Recurse -Force
Remove-Item -Recurse -Force dsfull
$size = (Get-ChildItem -Recurse -Force ds64 | Measure-Object -Property Length -Sum).Sum
Write-Host ("Cached DS library set: {0:N1} MB" -f ($size / 1MB))
# steamcmd can exit non-zero even after a successful install; the success
# check above already validated the DS install, so do not let a stray
# $LASTEXITCODE from steamcmd fail this step.
exit 0
- name: Install Magnetar PluginSdk reference
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = 'Stop'
$release = gh api repos/CometWorks/magnetar/releases/latest | ConvertFrom-Json
if ($release.draft -or $release.prerelease) {
throw "latest Magnetar release is not a full release"
}
$winAssets = @($release.assets | Where-Object { $_.name -like 'MagnetarForWindows-*.7z' })
if ($winAssets.Count -ne 1) {
$names = ($release.assets | ForEach-Object { $_.name }) -join ', '
throw "expected one MagnetarForWindows-*.7z asset, found $($winAssets.Count): $names"
}
$magnetarRoot = Join-Path $env:APPDATA 'Magnetar'
New-Item -ItemType Directory -Force -Path $magnetarRoot | Out-Null
Invoke-WebRequest -Uri $winAssets[0].browser_download_url -OutFile magnetar.7z
& 7z x magnetar.7z "-o$magnetarRoot" -y
if ($LASTEXITCODE -ne 0) { throw "7z extraction failed with exit code $LASTEXITCODE" }
$pluginSdk = Get-ChildItem -Path $magnetarRoot -Recurse -Filter PluginSdk.dll | Select-Object -First 1
if (-not $pluginSdk) { throw "PluginSdk.dll not found in Magnetar archive" }
"MagnetarBin=$($pluginSdk.DirectoryName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding ascii
- name: Package Windows release
shell: pwsh
env:
DS64: ${{ github.workspace }}\ds64
run: scripts/package-windows-release.ps1
- name: Verify Windows installer archive root
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path 'artifacts/windows/quasar-installer-windows.zip').Path)
try {
$roots = @(
$zip.Entries |
Where-Object { -not [string]::IsNullOrWhiteSpace($_.FullName) } |
ForEach-Object { ($_.FullName -replace '\\', '/').Split('/')[0] } |
Sort-Object -Unique
)
}
finally {
$zip.Dispose()
}
if ($roots.Count -ne 1 -or $roots[0] -ne 'Quasar') {
throw "expected quasar-installer-windows.zip root directory to be Quasar; found: $($roots -join ', ')"
}
- name: Upload Windows archives
uses: actions/upload-artifact@v4
with:
name: quasar-windows
path: |
artifacts/windows/quasar-web-win-x64.zip
artifacts/windows/quasar-installer-windows.zip
# Publish a single GitHub release/tag carrying every Linux and Windows archive
# plus one combined SHA256SUMS. The updater finds the asset it needs by name,
# so all platforms can share the same release.
release:
needs: [metadata, build-linux, build-windows]
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: dist
- name: Assemble combined release files
run: |
set -euo pipefail
mkdir -p release
for asset in \
quasar-installer-linux.tar.gz \
quasar-web-linux-x64.tar.gz \
quasar-installer-windows.zip \
quasar-web-win-x64.zip; do
found="$(find dist -type f -name "$asset" -print -quit)"
if [ -z "$found" ]; then
echo "ERROR: expected release archive not found: $asset" >&2
exit 1
fi
cp "$found" "release/$asset"
done
cd release
sha256sum \
quasar-installer-linux.tar.gz \
quasar-web-linux-x64.tar.gz \
quasar-installer-windows.zip \
quasar-web-win-x64.zip > SHA256SUMS
echo "Combined release archives:"; ls -1
echo "Combined SHA256SUMS:"; cat SHA256SUMS
- name: Publish combined release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.metadata.outputs.tag }}
target_commitish: ${{ github.sha }}
name: ${{ needs.metadata.outputs.name }}
draft: ${{ needs.metadata.outputs.draft }}
prerelease: ${{ needs.metadata.outputs.prerelease }}
body: |
## Install Quasar
Download the single archive for your operating system:
| Operating system | Download |
| --- | --- |
| **Linux** (x64) | **`quasar-installer-linux.tar.gz`** |
| **Windows** (x64) | **`quasar-installer-windows.zip`** |
Download the archive with **installer** in its name. It contains one top-level `Quasar` folder with the Quasar **Bootstrap launcher**, the install/uninstall scripts, and a default `appsettings.json`. Follow the [Linux](https://github.com/CometWorks/quasar/blob/main/Docs/LinuxDeploymentAndUpdates.md) or [Windows](https://github.com/CometWorks/quasar/blob/main/Docs/WindowsDeploymentAndUpdates.md) deployment guide to install it. The web UI is available on http://127.0.0.1:8080 by default (the web server listens on all interfaces at 0.0.0.0:8080).
The other two archives (`quasar-web-linux-x64.tar.gz` and `quasar-web-win-x64.zip`) are updates that Quasar downloads and applies automatically — you never download them by hand.
## Why are there two archives per operating system?
Quasar deploys as two layers — a small, stable **Bootstrap launcher** (the installer) and a larger, frequently-updated **web UI worker** — shipped separately so the launcher can update the web UI, and itself, in place with no manual reinstall.
files: |
release/quasar-installer-linux.tar.gz
release/quasar-web-linux-x64.tar.gz
release/quasar-installer-windows.zip
release/quasar-web-win-x64.zip
release/SHA256SUMS