Production installer smoke #512
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: 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 |