Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/install-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Install Test

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
installer-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: python -m pip install PyYAML==6.0.3
- run: python -m unittest discover -s tests -v

install:
name: Install (${{ matrix.runner }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
binary_arch: x86-64
- runner: ubuntu-24.04-arm
binary_arch: aarch64
- runner: macos-latest
binary_arch: arm64
- runner: macos-26-intel
binary_arch: x86_64
- runner: windows-latest
binary_arch: x86-64
- runner: windows-11-arm
binary_arch: Aarch64|ARM64
steps:
- uses: actions/checkout@v7
- name: Install and run info
uses: ./
with:
command: info
- name: Verify installed architecture and PATH
shell: bash
env:
EXPECTED_ARCH: ${{ matrix.binary_arch }}
run: |
binary=$(command -v stackql-deploy)
if [ "$RUNNER_OS" = Windows ]; then binary="${binary%.exe}.exe"; fi
file "$binary"
file -b "$binary" | grep -E "$EXPECTED_ARCH"
stackql-deploy --version
git hash-object "$binary" > "$RUNNER_TEMP/stackql-deploy.hash"
- name: Reuse installed binary
uses: ./
with:
command: info
- name: Verify binary is unchanged
shell: bash
run: |
binary=$(command -v stackql-deploy)
if [ "$RUNNER_OS" = Windows ]; then binary="${binary%.exe}.exe"; fi
git hash-object "$binary" | cmp - "$RUNNER_TEMP/stackql-deploy.hash"
2 changes: 2 additions & 0 deletions .github/workflows/stackql-deploy-actions-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
stack_env: ci-macos-intel
- runner: windows-latest
stack_env: ci-windows-amd64
- runner: windows-11-arm
stack_env: ci-windows-arm64
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ Github Action to execute `stackql-deploy` to deploy or test a stack. [`stackql-

# Usage

## Supported platforms

The action downloads the latest prebuilt release for Linux, macOS, and Windows
on x86_64 and ARM64. macOS uses the universal binary; no Rust toolchain is needed.
Windows detects the native CPU even when Git Bash runs under x64 emulation.

| Platform | Architecture | CI runner |
|----------|--------------|-----------|
| Linux | x86_64 | `ubuntu-latest` |
| Linux | ARM64 | `ubuntu-24.04-arm` |
| macOS | Apple Silicon | `macos-latest` |
| macOS | Intel | `macos-26-intel` |
| Windows | x86_64 | `windows-latest` |
| Windows | ARM64 | `windows-11-arm` |

CI checks installation, binary architecture, and reuse on each runner without
cloud credentials. The AWS SSM workflow also tests deployment, updates, and
teardown on each runner.

## Provider Authentication
Authentication to StackQL providers is done via environment variables source from GitHub Actions Secrets. To learn more about authentication, see the setup instructions for your provider or providers at the [StackQL Provider Registry Docs](https://stackql.io/registry).

Expand Down
78 changes: 44 additions & 34 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,46 +51,56 @@ runs:
else
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
MINGW*|MSYS*|CYGWIN*|Windows_NT)
# The registry reports the native CPU even in an emulated x64 shell.
WINDOWS_ARCH=$(powershell.exe -NoProfile -NonInteractive -Command "(Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name PROCESSOR_ARCHITECTURE -ErrorAction Stop).PROCESSOR_ARCHITECTURE" 2>/dev/null || true)
WINDOWS_ARCH="${WINDOWS_ARCH//$'\r'/}"
ARCH="${WINDOWS_ARCH:-${PROCESSOR_ARCHITEW6432:-${PROCESSOR_ARCHITECTURE:-$ARCH}}}"
OS="Windows"
;;
esac
case "$ARCH" in
x86_64|amd64|AMD64|x64|X64) ARCH="x86_64" ;;
aarch64|arm64|ARM64) ARCH="arm64" ;;
esac
echo "Detected platform: ${OS}-${ARCH}"

if [ "$OS" = "Darwin" ]; then
# macOS: build from crates.io. Prebuilt macOS assets have had naming drift
# (universal vs arch-specific), so cargo install is the reliable path here.
echo "Installing stackql-deploy via cargo..."
cargo install stackql-deploy --locked
echo "${HOME}/.cargo/bin" >> "$GITHUB_PATH"
export PATH="${HOME}/.cargo/bin:${PATH}"
BASE_URL="https://github.com/stackql/stackql-deploy-rs/releases/latest/download"
BINARY="stackql-deploy"
case "${OS}-${ARCH}" in
Linux-x86_64|Linux-arm64)
ASSET="stackql-deploy-linux-${ARCH}.tar.gz" ;;
Darwin-x86_64|Darwin-arm64)
ASSET="stackql-deploy-macos-universal.tar.gz" ;;
Windows-x86_64|Windows-arm64)
ASSET="stackql-deploy-windows-${ARCH}.zip"
BINARY="stackql-deploy.exe"
;;
*)
echo "Unsupported platform: ${OS}-${ARCH}" >&2
exit 1
;;
esac
URL="${BASE_URL}/${ASSET}"
echo "Download URL: ${URL}"

if [ "$OS" = "Windows" ]; then
curl -fL -o stackql-deploy.zip "$URL"
unzip -o stackql-deploy.zip
rm -f stackql-deploy.zip
else
echo "Installing stackql-deploy binary..."
BASE_URL="https://github.com/stackql/stackql-deploy-rs/releases/latest/download"
case "${OS}-${ARCH}" in
Linux-x86_64) URL="${BASE_URL}/stackql-deploy-linux-x86_64.tar.gz" ;;
Linux-aarch64) URL="${BASE_URL}/stackql-deploy-linux-arm64.tar.gz" ;;
MINGW*|MSYS*|CYGWIN*)
URL="${BASE_URL}/stackql-deploy-windows-x86_64.zip" ;;
*)
echo "Unsupported platform: ${OS}-${ARCH}"
exit 1
;;
esac
echo "Download URL: ${URL}"

if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then
curl -fL -o stackql-deploy.zip "$URL"
unzip -o stackql-deploy.zip
rm -f stackql-deploy.zip
else
curl -fL "$URL" | tar -xz
fi

chmod +x ./stackql-deploy
echo "Binary details: $(file ./stackql-deploy)"
echo "${GITHUB_WORKSPACE}" >> "$GITHUB_PATH"
export PATH="${GITHUB_WORKSPACE}:${PATH}"
curl -fL "$URL" | tar -xz
fi

chmod +x "./${BINARY}"
echo "Binary details: $(file "./${BINARY}")"
echo "${GITHUB_WORKSPACE}" >> "$GITHUB_PATH"
PATH="$(pwd):${PATH}"
export PATH

echo "stackql-deploy installed:"
stackql-deploy info
"./${BINARY}" info
fi

- name: Run stackql-deploy
Expand Down
195 changes: 195 additions & 0 deletions tests/test_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import io
import os
from pathlib import Path
import shutil
import subprocess
import tarfile
import tempfile
import unittest
import zipfile

import yaml


ROOT = Path(__file__).resolve().parents[1]
INSTALL = yaml.safe_load((ROOT / "action.yml").read_text(encoding="utf-8"))["runs"]["steps"][0]["run"]
BASE_URL = "https://github.com/stackql/stackql-deploy-rs/releases/latest/download"
BINARY = b'''#!/usr/bin/env bash
printf '%s\n' "$*" >> "$BINARY_LOG"
if [ "$1" = "info" ]; then exit "${MOCK_INFO_STATUS:-0}"; fi
echo 'stackql-deploy 2.3.0'
'''
MOCKS = r'''
export PROCESSOR_ARCHITECTURE="$MOCK_PROCESSOR_ARCHITECTURE"
export PROCESSOR_ARCHITEW6432="$MOCK_PROCESSOR_ARCHITEW6432"
uname() {
case "$1" in
-s) echo "$MOCK_OS" ;;
-m) echo "$MOCK_ARCH" ;;
*) return 1 ;;
esac
}
powershell.exe() {
printf '%s\n' "$*" >> "$POWERSHELL_LOG"
[ -n "$MOCK_WINDOWS_ARCH" ] || return 1
printf '%s\r\n' "$MOCK_WINDOWS_ARCH"
}
curl() {
local url="${@: -1}"
echo "$url" >> "$DOWNLOAD_LOG"
[ "$MOCK_DOWNLOAD_STATUS" = 0 ] || return "$MOCK_DOWNLOAD_STATUS"
if [ "$2" = '-o' ]; then
cp "$ARCHIVES/${url##*/}" "$3"
else
cat "$ARCHIVES/${url##*/}"
fi
}
if [ -n "${PREINSTALLED_DIR:-}" ]; then
export PATH="$(cd "$PREINSTALLED_DIR" && pwd):$PATH"
fi
'''


class InstallTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="deploy install ")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.workspace = self.root / "workspace with spaces"
self.workspace.mkdir()
self.archives = self.root / "archives"
self.archives.mkdir()
for platform in ("linux-x86_64", "linux-arm64", "macos-universal"):
with tarfile.open(self.archives / f"stackql-deploy-{platform}.tar.gz", "w:gz") as archive:
member = tarfile.TarInfo("stackql-deploy")
member.size = len(BINARY)
member.mode = 0o644
archive.addfile(member, io.BytesIO(BINARY))
for arch in ("x86_64", "arm64"):
with zipfile.ZipFile(self.archives / f"stackql-deploy-windows-{arch}.zip", "w") as archive:
archive.writestr("stackql-deploy.exe", BINARY)
self.script = self.root / "install.sh"
self.script.write_text(MOCKS + INSTALL, encoding="utf-8", newline="\n")
self.env = {
**os.environ,
"PATH": "/usr/bin:/bin",
"GITHUB_WORKSPACE": self.workspace.as_posix(),
"GITHUB_PATH": (self.root / "github_path").as_posix(),
"ARCHIVES": self.archives.as_posix(),
"DOWNLOAD_LOG": (self.root / "downloads").as_posix(),
"BINARY_LOG": (self.root / "binary_calls").as_posix(),
"POWERSHELL_LOG": (self.root / "powershell_calls").as_posix(),
"MOCK_WINDOWS_ARCH": "",
"PROCESSOR_ARCHITECTURE": "",
"PROCESSOR_ARCHITEW6432": "",
"MOCK_DOWNLOAD_STATUS": "0",
"MOCK_INFO_STATUS": "0",
"PREINSTALLED_DIR": "",
}

def install(self, system, arch, **env):
for name in ("stackql-deploy", "stackql-deploy.exe", "stackql-deploy.zip"):
(self.workspace / name).unlink(missing_ok=True)
return subprocess.run(
[shutil.which("bash"), "--noprofile", "--norc", "-e", "-o", "pipefail", self.script.as_posix()],
cwd=self.workspace,
env={
**self.env, "MOCK_OS": system, "MOCK_ARCH": arch, **env,
"MOCK_PROCESSOR_ARCHITECTURE": env.get("PROCESSOR_ARCHITECTURE", ""),
"MOCK_PROCESSOR_ARCHITEW6432": env.get("PROCESSOR_ARCHITEW6432", ""),
},
text=True,
capture_output=True,
)

def assert_install(self, system, arch, asset, **env):
result = self.install(system, arch, **env)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual((self.root / "downloads").read_text().splitlines()[-1], f"{BASE_URL}/{asset}")
binary = "stackql-deploy.exe" if asset.endswith(".zip") else "stackql-deploy"
self.assertEqual((self.workspace / binary).read_bytes(), BINARY)
self.assertIn("info", (self.root / "binary_calls").read_text().splitlines())
self.assertEqual((self.root / "github_path").read_text().splitlines()[-1], self.workspace.as_posix())
self.assertFalse((self.workspace / "stackql-deploy.zip").exists())

def test_linux_and_macos_architectures(self):
for system, arch, platform in (
("Linux", "x86_64", "linux-x86_64"),
("Linux", "amd64", "linux-x86_64"),
("Linux", "aarch64", "linux-arm64"),
("Linux", "arm64", "linux-arm64"),
("Darwin", "x86_64", "macos-universal"),
("Darwin", "arm64", "macos-universal"),
):
with self.subTest(system=system, arch=arch):
self.assert_install(system, arch, f"stackql-deploy-{platform}.tar.gz")
self.assertFalse((self.root / "powershell_calls").exists())

def test_windows_native_architecture(self):
for system in ("MINGW64_NT-10.0", "MSYS_NT-10.0", "CYGWIN_NT-10.0", "Windows_NT"):
for native, asset_arch in (("AMD64", "x86_64"), ("ARM64", "arm64")):
with self.subTest(system=system, native=native):
self.assert_install(
system, "x86_64", f"stackql-deploy-windows-{asset_arch}.zip",
MOCK_WINDOWS_ARCH=native,
PROCESSOR_ARCHITECTURE="AMD64",
PROCESSOR_ARCHITEW6432="AMD64",
)
self.assertIn("Get-ItemProperty", (self.root / "powershell_calls").read_text())

def test_windows_architecture_fallbacks(self):
for arch, env, expected in (
("x86_64", {"PROCESSOR_ARCHITEW6432": "ARM64", "PROCESSOR_ARCHITECTURE": "AMD64"}, "arm64"),
("x86_64", {"PROCESSOR_ARCHITECTURE": "ARM64"}, "arm64"),
("x86_64", {"PROCESSOR_ARCHITECTURE": "AMD64"}, "x86_64"),
("aarch64", {}, "arm64"),
("x86_64", {}, "x86_64"),
):
with self.subTest(arch=arch, env=env):
self.assert_install("MINGW64_NT-10.0", arch, f"stackql-deploy-windows-{expected}.zip", **env)

def test_unsupported_platforms_do_not_download(self):
for system, arch in (("Linux", "riscv64"), ("Darwin", "ppc"), ("MINGW32_NT-10.0", "i686"), ("FreeBSD", "x86_64")):
with self.subTest(system=system, arch=arch):
result = self.install(system, arch)
self.assertNotEqual(result.returncode, 0)
self.assertIn("Unsupported platform", result.stderr)
self.assertFalse((self.root / "downloads").exists())
self.assertFalse((self.root / "github_path").exists())

def test_download_failures_stop_installation(self):
for system in ("Linux", "MINGW64_NT-10.0"):
with self.subTest(system=system):
result = self.install(system, "x86_64", MOCK_DOWNLOAD_STATUS="22")
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.root / "binary_calls").exists())
self.assertFalse((self.root / "github_path").exists())

def test_corrupt_archives_stop_installation(self):
for system, asset in (("Linux", "linux-x86_64.tar.gz"), ("MINGW64_NT-10.0", "windows-x86_64.zip")):
with self.subTest(system=system):
(self.archives / f"stackql-deploy-{asset}").write_bytes(b"invalid archive")
result = self.install(system, "x86_64")
self.assertNotEqual(result.returncode, 0)
self.assertFalse((self.root / "binary_calls").exists())
self.assertFalse((self.root / "github_path").exists())

def test_info_failure_is_propagated(self):
result = self.install("Linux", "x86_64", MOCK_INFO_STATUS="9")
self.assertEqual(result.returncode, 9, result.stdout + result.stderr)

def test_existing_install_is_reused(self):
installed = self.root / "existing install"
installed.mkdir()
binary = installed / "stackql-deploy"
binary.write_bytes(BINARY)
binary.chmod(0o755)
result = self.install("Linux", "x86_64", PREINSTALLED_DIR=installed.as_posix())
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("already installed", result.stdout)
self.assertFalse((self.root / "downloads").exists())
self.assertEqual((self.root / "binary_calls").read_text().splitlines(), ["--version"])


if __name__ == "__main__":
unittest.main()
Loading