fix(viewer): resolve workshop mod roots (#110) #177
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |