Skip to content

Production installer smoke #516

Production installer smoke

Production installer smoke #516

name: Production installer smoke
on:
workflow_dispatch:
schedule:
- cron: "23 7 * * *"
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
env:
SPARK_INSTALL_HOST: https://agent.sparkswarm.ai
jobs:
production-metadata:
name: production-metadata
runs-on: ubuntu-latest
steps:
- name: Fetch and validate production metadata
shell: bash
run: |
set -euo pipefail
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT
curl -fsSL "$SPARK_INSTALL_HOST/install/checksums.txt" -o "$workdir/checksums.txt"
curl -fsSL "$SPARK_INSTALL_HOST/install/checksums.json" -o "$workdir/checksums.json"
curl -fsSL "$SPARK_INSTALL_HOST/install/commands.json" -o "$workdir/commands.json"
curl -fsSL "$SPARK_INSTALL_HOST/install/release-manifest.json" -o "$workdir/release-manifest.json"
curl -fsSI "$SPARK_INSTALL_HOST/" -o "$workdir/root.headers"
curl -fsSI "$SPARK_INSTALL_HOST/install.sh" -o "$workdir/install-sh.headers"
curl -fsSI "$SPARK_INSTALL_HOST/install/release-manifest.json" -o "$workdir/release-manifest.headers"
curl -fsSI "$SPARK_INSTALL_HOST/docs" -o "$workdir/docs.headers"
python3 - "$workdir" <<'PY'
import json
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
expected = {}
for line in (root / "checksums.txt").read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
digest, relpath = line.split(maxsplit=1)
if len(digest) != 64:
raise SystemExit(f"{relpath} digest is not sha256")
expected[relpath] = digest
checksums = json.loads((root / "checksums.json").read_text(encoding="utf-8"))
commands = json.loads((root / "commands.json").read_text(encoding="utf-8"))
manifest = json.loads((root / "release-manifest.json").read_text(encoding="utf-8"))
json_hashes = {entry["path"]: entry["sha256"] for entry in checksums["files"]}
command_hashes = commands["checksums"]["sha256"]
if json_hashes != expected:
raise SystemExit("production checksums.json does not match checksums.txt")
if command_hashes != expected:
raise SystemExit("production commands.json does not match checksums.txt")
manifest_ref = manifest["sparkCli"].get("ref", manifest["sparkCli"]["commit"])
if commands["source"]["ref"] != manifest_ref:
raise SystemExit("production source ref does not match release manifest")
if commands["source"].get("commit") and commands["source"]["commit"] != manifest["sparkCli"]["commit"]:
raise SystemExit("production source commit does not match release manifest")
if not all(len(value) == 64 for value in manifest["runtimes"]["uv"]["assets"].values()):
raise SystemExit("production uv asset hashes must be sha256 digests")
if manifest["networkAllowlist"] != [
"agent.sparkswarm.ai",
"github.com/vibeforge1111/spark-cli",
"github.com/astral-sh/uv",
"nodejs.org",
]:
raise SystemExit("production network allowlist changed unexpectedly")
required_headers = {
"root.headers": {
"content-type": "text/html",
"strict-transport-security": "max-age=31536000",
"content-security-policy": "default-src 'self'",
"x-content-type-options": "nosniff",
"referrer-policy": "strict-origin-when-cross-origin",
"cache-control": "no-cache",
},
"install-sh.headers": {
"content-type": "text/plain",
"strict-transport-security": "max-age=31536000",
"content-security-policy": "default-src 'self'",
"x-content-type-options": "nosniff",
"cache-control": "no-cache",
},
"release-manifest.headers": {
"content-type": "application/json",
"strict-transport-security": "max-age=31536000",
"content-security-policy": "default-src 'self'",
"x-content-type-options": "nosniff",
"cache-control": "no-cache",
},
"docs.headers": {
"content-type": "text/html",
"strict-transport-security": "max-age=31536000",
"content-security-policy": "default-src 'self'",
"x-content-type-options": "nosniff",
"cache-control": "no-cache",
},
}
for filename, expected_headers in required_headers.items():
headers = {}
for line in (root / filename).read_text(encoding="utf-8").splitlines():
if ":" not in line:
continue
name, value = line.split(":", 1)
headers[name.strip().lower()] = value.strip().lower()
for name, expected_fragment in expected_headers.items():
actual = headers.get(name, "")
if expected_fragment.lower() not in actual:
raise SystemExit(f"{filename} missing {name}: {expected_fragment}")
print("production metadata ok")
PY
production-installer-dry-run:
name: production-installer-dry-run (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Production Bash dry-run
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
curl -fsSL "$SPARK_INSTALL_HOST/install.sh" -o spark-install.sh
curl -fsSL "$SPARK_INSTALL_HOST/install/checksums.txt" -o checksums.txt
expected="$(tr -d '\r' < checksums.txt | awk '$2 == "install.sh" {print $1}')"
actual="$(if command -v sha256sum >/dev/null 2>&1; then sha256sum spark-install.sh | awk '{print $1}'; else shasum -a 256 spark-install.sh | awk '{print $1}'; fi)"
test "$actual" = "$expected"
bash -n spark-install.sh
SPARK_PREFIX="$RUNNER_TEMP/spark-production-dry-run" bash spark-install.sh --dry-run
- name: Production PowerShell dry-run
if: runner.os == 'Windows'
shell: pwsh
run: |
Invoke-WebRequest -Uri "$env:SPARK_INSTALL_HOST/install.ps1" -OutFile ".\spark-install.ps1"
Invoke-WebRequest -Uri "$env:SPARK_INSTALL_HOST/install/checksums.txt" -OutFile ".\checksums.txt"
$expected = (Get-Content .\checksums.txt | Where-Object { $_ -match '\sinstall\.ps1$' } | ForEach-Object { ($_ -split '\s+')[0] })
if (-not $expected) { throw "install.ps1 checksum missing from production checksums.txt" }
$actual = (Get-FileHash .\spark-install.ps1 -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) { throw "install.ps1 checksum mismatch: $actual != $expected" }
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path ".\spark-install.ps1"), [ref] $tokens, [ref] $errors) | Out-Null
if ($errors.Count) {
$errors | ForEach-Object { Write-Error $_ }
exit 1
}
powershell -NoProfile -ExecutionPolicy Bypass -File .\spark-install.ps1 -Prefix "$env:RUNNER_TEMP\spark-production-dry-run" -DryRun
production-installer-real-install:
name: production-installer-real-install (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Production Bash real install
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
curl -fsSL "$SPARK_INSTALL_HOST/install.sh" -o spark-install.sh
curl -fsSL "$SPARK_INSTALL_HOST/install/checksums.txt" -o checksums.txt
expected="$(tr -d '\r' < checksums.txt | awk '$2 == "install.sh" {print $1}')"
actual="$(if command -v sha256sum >/dev/null 2>&1; then sha256sum spark-install.sh | awk '{print $1}'; else shasum -a 256 spark-install.sh | awk '{print $1}'; fi)"
test "$actual" = "$expected"
prefix="$RUNNER_TEMP/spark-production-real-install"
SPARK_PREFIX="$prefix" bash spark-install.sh --yes --skip-setup --no-autostart --no-shell-profile --managed-node
test -x "$prefix/bin/spark"
"$prefix/bin/spark" --help >/dev/null
"$prefix/bin/spark" guide >/dev/null
rm -rf "$prefix"
- name: Production PowerShell real install
if: runner.os == 'Windows'
shell: pwsh
run: |
Invoke-WebRequest -Uri "$env:SPARK_INSTALL_HOST/install.ps1" -OutFile ".\spark-install.ps1"
Invoke-WebRequest -Uri "$env:SPARK_INSTALL_HOST/install/checksums.txt" -OutFile ".\checksums.txt"
$expected = (Get-Content .\checksums.txt | Where-Object { $_ -match '\sinstall\.ps1$' } | ForEach-Object { ($_ -split '\s+')[0] })
if (-not $expected) { throw "install.ps1 checksum missing from production checksums.txt" }
$actual = (Get-FileHash .\spark-install.ps1 -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) { throw "install.ps1 checksum mismatch: $actual != $expected" }
$prefix = Join-Path $env:RUNNER_TEMP "spark-production-real-install"
powershell -NoProfile -ExecutionPolicy Bypass -File .\spark-install.ps1 -Prefix $prefix -Yes -SkipSetup -NoAutostart -ManagedNode
if (-not (Test-Path (Join-Path $prefix "bin\spark.cmd"))) { throw "spark.cmd wrapper missing" }
& (Join-Path $prefix "bin\spark.cmd") --help | Out-Null
& (Join-Path $prefix "bin\spark.cmd") guide | Out-Null
Remove-Item -LiteralPath $prefix -Recurse -Force