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
11 changes: 10 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@
"Read(//c/msys64/mingw64/bin/**)",
"Read(//c/msys64/clang64/bin/**)",
"Bash(cargo doc:*)",
"Bash(grep -r impl.*Read.*for.*Response c:/LocalGitRepos/stackql/stackql-deploy-rs/target/doc/reqwest/blocking/struct.Response.html)"
"Bash(grep -r impl.*Read.*for.*Response c:/LocalGitRepos/stackql/stackql-deploy-rs/target/doc/reqwest/blocking/struct.Response.html)",
"Bash(grep -r \"multiarch\\\\|darwin\\\\|amd64\\\\|architecture\" --include=*.toml --include=*.rs --include=*.md)",
"WebFetch(domain:releases.stackql.io)",
"Bash(curl -sI -L \"https://storage.googleapis.com/stackql-public-releases/latest/stackql_darwin_multiarch.pkg\")",
"Bash(curl -sI -L \"https://releases.stackql.io/stackql/latest/stackql_linux_arm64.zip\")",
"Bash(curl -sI -L \"https://releases.stackql.io/stackql/latest/stackql_darwin_multiarch.pkg\")",
"Bash(python -c \"import yaml; yaml.safe_load\\(open\\(''.github/workflows/ci.yml''\\)\\); print\\(''ci.yml OK''\\)\")",
"Bash(python -c \"import yaml; yaml.safe_load\\(open\\(''.github/workflows/release.yml''\\)\\); print\\(''release.yml OK''\\)\")",
"Bash(cargo fmt:*)",
"Bash(rustfmt --check src/utils/download.rs src/utils/server.rs)"
]
}
}
72 changes: 64 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: ['**']
branches: [main]
paths:
- 'src/**'
- 'build.rs'
Expand All @@ -23,10 +23,10 @@ env:

jobs:

# Runs on: push to any non-main branch, PR to main
# Runs on: PR to main
lint:
name: Lint
if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref != 'refs/heads/main')
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -37,10 +37,10 @@ jobs:
- name: Run lint
run: bash ci-scripts/lint.sh

# Runs on: push to any non-main branch, PR to main
# Runs on: PR to main
test:
name: Tests
if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref != 'refs/heads/main')
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -49,7 +49,7 @@ jobs:
- name: Run tests
run: bash ci-scripts/test.sh

# Runs on: PR to main only — single-platform release build to catch compile errors pre-merge
# Runs on: PR to main only — fast compile check (no codegen) to catch errors pre-merge
build-check:
name: Build Check
if: github.event_name == 'pull_request'
Expand All @@ -61,8 +61,8 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Create placeholder contributors file
run: touch contributors.csv
- name: Build release
run: bash ci-scripts/build.sh
- name: Check compilation
run: cargo check --release

# Runs on: push to main — fetch contributors for build-time injection
prepare:
Expand Down Expand Up @@ -170,3 +170,59 @@ jobs:
name: ${{ matrix.artifact-name }}
path: ${{ matrix.artifact-name }}.*
if-no-files-found: error

# Runs on: push to main — runtime smoke test on each platform after build
runtime-test:
name: Runtime Test (${{ matrix.os }})
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
artifact-name: stackql-deploy-linux-x86_64
archive: tar.gz
- os: windows-latest
artifact-name: stackql-deploy-windows-x86_64
archive: zip
- os: macos-latest
artifact-name: stackql-deploy-macos-arm64
archive: tar.gz
- os: macos-13
artifact-name: stackql-deploy-macos-x86_64
archive: tar.gz
runs-on: ${{ matrix.os }}
steps:
- uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact-name }}
- name: Extract binary (tar.gz)
if: matrix.archive == 'tar.gz'
run: tar -xzf ${{ matrix.artifact-name }}.tar.gz
- name: Extract binary (zip)
if: matrix.archive == 'zip'
shell: pwsh
run: Expand-Archive -Path ${{ matrix.artifact-name }}.zip -DestinationPath .
- name: Run smoke test
shell: bash
run: |
set -e

echo "=== Test 1: stackql-deploy info (forces stackql download) ==="
./stackql-deploy info

echo ""
echo "=== Test 2: stackql-deploy start-server ==="
./stackql-deploy start-server

echo ""
echo "=== Test 3: stackql-deploy info (verify server running) ==="
./stackql-deploy info

echo ""
echo "=== Test 4: stackql-deploy stop-server ==="
./stackql-deploy stop-server

echo ""
echo "=== All smoke tests passed ==="
57 changes: 56 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,64 @@ jobs:
path: ${{ matrix.artifact-name }}.*
if-no-files-found: error

# Runtime smoke test on each platform before releasing
runtime-test:
name: Runtime Test (${{ matrix.os }})
needs: build
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
artifact-name: stackql-deploy-linux-x86_64
archive: tar.gz
- os: windows-latest
artifact-name: stackql-deploy-windows-x86_64
archive: zip
- os: macos-latest
artifact-name: stackql-deploy-macos-arm64
archive: tar.gz
- os: macos-13
artifact-name: stackql-deploy-macos-x86_64
archive: tar.gz
runs-on: ${{ matrix.os }}
steps:
- uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact-name }}
- name: Extract binary (tar.gz)
if: matrix.archive == 'tar.gz'
run: tar -xzf ${{ matrix.artifact-name }}.tar.gz
- name: Extract binary (zip)
if: matrix.archive == 'zip'
shell: pwsh
run: Expand-Archive -Path ${{ matrix.artifact-name }}.zip -DestinationPath .
- name: Run smoke test
shell: bash
run: |
set -e

echo "=== Test 1: stackql-deploy info (forces stackql download) ==="
./stackql-deploy info

echo ""
echo "=== Test 2: stackql-deploy start-server ==="
./stackql-deploy start-server

echo ""
echo "=== Test 3: stackql-deploy info (verify server running) ==="
./stackql-deploy info

echo ""
echo "=== Test 4: stackql-deploy stop-server ==="
./stackql-deploy stop-server

echo ""
echo "=== All smoke tests passed ==="

release:
name: Create GitHub Release
needs: build
needs: [build, runtime-test]
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[package]
name = "stackql-deploy"
version = "2.0.2"
version = "2.0.3"
edition = "2021"
rust-version = "1.75"
description = "Infrastructure-as-code framework for declarative cloud resource management using StackQL"
authors = ["StackQL Studios <info@stackql.io>"]
license = "MIT"
homepage = "https://stackql.io"
repository = "https://github.com/stackql/stackql-deploy"
repository = "https://github.com/stackql/stackql-deploy-rs"
keywords = ["stackql", "infrastructure", "iac", "cloud", "devops"]
categories = ["command-line-utilities", "development-tools"]
readme = "README.md"
Expand Down
20 changes: 2 additions & 18 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,8 @@ pub const STACKQL_BINARY_NAME: &str = "stackql.exe";
#[cfg(not(target_os = "windows"))]
pub const STACKQL_BINARY_NAME: &str = "stackql";

/// StackQL download URLs by platform
#[cfg_attr(
target_os = "windows",
doc = "StackQL download URL (platform dependent)"
)]
#[cfg(target_os = "windows")]
pub const STACKQL_DOWNLOAD_URL: &str =
"https://releases.stackql.io/stackql/latest/stackql_windows_amd64.zip";

#[cfg_attr(target_os = "linux", doc = "StackQL download URL (platform dependent)")]
#[cfg(target_os = "linux")]
pub const STACKQL_DOWNLOAD_URL: &str =
"https://releases.stackql.io/stackql/latest/stackql_linux_amd64.zip";

#[cfg_attr(target_os = "macos", doc = "StackQL download URL (platform dependent)")]
#[cfg(target_os = "macos")]
pub const STACKQL_DOWNLOAD_URL: &str =
"https://storage.googleapis.com/stackql-public-releases/latest/stackql_darwin_multiarch.pkg";
/// Base URL for StackQL releases
pub const STACKQL_RELEASE_BASE_URL: &str = "https://releases.stackql.io/stackql/latest";

/// Commands exempt from binary check
pub const EXEMPT_COMMANDS: [&str; 1] = ["init"];
Expand Down
75 changes: 64 additions & 11 deletions src/utils/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,37 @@ use log::debug;
use reqwest::blocking::Client;
use zip::ZipArchive;

use crate::app::STACKQL_DOWNLOAD_URL;
use crate::app::STACKQL_RELEASE_BASE_URL;
use crate::error::AppError;
use crate::utils::platform::{get_platform, Platform};

/// Retrieves the URL for downloading the StackQL binary.
/// Retrieves the URL for downloading the StackQL binary based on OS and architecture.
pub fn get_download_url() -> Result<String, AppError> {
Ok(STACKQL_DOWNLOAD_URL.to_string())
let platform = get_platform();
match platform {
Platform::MacOS => Ok(format!(
"{}/stackql_darwin_multiarch.pkg",
STACKQL_RELEASE_BASE_URL
)),
Platform::Windows => Ok(format!(
"{}/stackql_windows_amd64.zip",
STACKQL_RELEASE_BASE_URL
)),
Platform::Linux => {
let arch = if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"amd64"
};
Ok(format!(
"{}/stackql_linux_{}.zip",
STACKQL_RELEASE_BASE_URL, arch
))
}
Platform::Unknown => Err(AppError::CommandFailed(
"Unsupported platform for stackql download".to_string(),
)),
}
}

/// Downloads the StackQL binary and extracts it to the current directory.
Expand Down Expand Up @@ -134,20 +158,32 @@ fn extract_binary(
}
fs::create_dir_all(&unpacked_dir).map_err(AppError::IoError)?;

Command::new("pkgutil")
let output = Command::new("pkgutil")
.arg("--expand-full")
.arg(archive_path)
.arg(&unpacked_dir)
.output()
.map_err(|e| AppError::CommandFailed(format!("Failed to extract pkg: {}", e)))?;

let extracted_binary = unpacked_dir
.join("payload")
.join("usr")
.join("local")
.join("bin")
.join("stackql");
fs::copy(extracted_binary, &binary_path).map_err(AppError::IoError)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AppError::CommandFailed(format!(
"pkgutil failed: {}",
stderr
)));
}

// Search for the stackql binary in the expanded pkg
// The structure can vary: payload/usr/local/bin/stackql or
// <subpkg>.pkg/payload/usr/local/bin/stackql
let extracted_binary =
find_file_recursive(&unpacked_dir, "stackql").ok_or_else(|| {
AppError::CommandFailed(
"Could not find stackql binary in extracted pkg".to_string(),
)
})?;

fs::copy(&extracted_binary, &binary_path).map_err(AppError::IoError)?;

// Clean up
fs::remove_dir_all(unpacked_dir).ok();
Expand Down Expand Up @@ -196,3 +232,20 @@ fn extract_binary(

Ok(binary_path)
}

/// Recursively search for a file by name in a directory tree.
/// Returns the first match that is a regular file (not a directory).
fn find_file_recursive(dir: &Path, target_name: &str) -> Option<PathBuf> {
let entries = fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = find_file_recursive(&path, target_name) {
return Some(found);
}
} else if path.file_name().and_then(|n| n.to_str()) == Some(target_name) {
return Some(path);
}
}
None
}
Loading