SDKsExamples & Workflows

Examples & Workflows

Practical, real-world workflows using the CiderStack SDKs.


CI/CD pipeline runner

Provision ephemeral macOS VMs for CI/CD pipelines — build, test, and tear down automatically.

JavaScript

import { FleetClient, VMState } from "@ciderstack/fleet-sdk";
 
const client = new FleetClient({
  host: process.env.FLEET_HOST!,
  apiToken: process.env.FLEET_TOKEN!,
});
 
async function runCIPipeline() {
  // 1. Create a disposable VM from a base image
  const vmId = await client.createVM({
    name: `ci-${Date.now()}`,
    cpuCount: 8,
    memoryMB: 16384,
    diskGB: 100,
    ociImage: "ghcr.io/myorg/macos-ci-base:latest",
  });
 
  // 2. Mark as disposable with 2-hour safety TTL
  await client.updateVMSettings(vmId, {
    intent: "disposable",
    ttlSeconds: 7200,
  });
 
  // 3. Wait for VM to boot
  let vm = await client.getVM(vmId);
  while (vm?.state !== VMState.Running) {
    await new Promise(r => setTimeout(r, 5000));
    vm = await client.getVM(vmId);
  }
 
  try {
    // 4. Build
    const build = await client.execCommand(vmId,
      "cd /Users/admin/project && xcodebuild -scheme MyApp build",
      { sshUser: "admin", sshPassword: "password", timeout: 600 },
    );
    if (!build.success) throw new Error(`Build failed: ${build.stderr}`);
 
    // 5. Test
    const test = await client.execCommand(vmId,
      "cd /Users/admin/project && xcodebuild test -scheme MyApp",
      { sshUser: "admin", sshPassword: "password", timeout: 600 },
    );
    console.log(test.stdout);
    return test.success;
  } finally {
    // 6. Tear down
    await client.stopVM(vmId);
    await client.deleteVM(vmId);
  }
}

Python

import os
import time
from ciderstack import FleetClient, VMState
 
client = FleetClient(
    os.environ["FLEET_HOST"],
    api_token=os.environ["FLEET_TOKEN"],
)
 
def run_ci_pipeline():
    # 1. Create a disposable VM
    vm_id = client.create_vm(
        name=f"ci-{int(time.time())}",
        cpu_count=8,
        memory_mb=16384,
        disk_gb=100,
        oci_image="ghcr.io/myorg/macos-ci-base:latest",
    )
 
    # 2. Mark as disposable with 2-hour safety TTL
    client.update_vm_settings(vm_id, intent="disposable", ttl_seconds=7200)
 
    # 3. Wait for VM to boot
    vm = client.get_vm(vm_id)
    while vm is None or vm.state != VMState.RUNNING:
        time.sleep(5)
        vm = client.get_vm(vm_id)
 
    try:
        # 4. Build
        build = client.exec_command(
            vm_id,
            "cd /Users/admin/project && xcodebuild -scheme MyApp build",
            ssh_user="admin", ssh_password="password", timeout=600,
        )
        if not build.success:
            raise RuntimeError(f"Build failed: {build.stderr}")
 
        # 5. Test
        test = client.exec_command(
            vm_id,
            "cd /Users/admin/project && xcodebuild test -scheme MyApp",
            ssh_user="admin", ssh_password="password", timeout=600,
        )
        print(test.stdout)
        return test.success
    finally:
        # 6. Tear down
        client.stop_vm(vm_id)
        client.delete_vm(vm_id)

Fleet monitoring

Poll fleet health and print a status report.

JavaScript

import { FleetClient } from "@ciderstack/fleet-sdk";
 
const client = new FleetClient({
  host: "192.168.1.100",
  apiToken: "csk_...",
});
 
async function printFleetStatus() {
  const overview = await client.getFleetOverview();
  const s = overview.stats;
 
  console.log("=== Fleet Status ===");
  console.log(`Nodes: ${s.connectedNodes}/${s.totalNodes}`);
  console.log(`VMs: ${s.runningVMs}/${s.totalVMs} running`);
  console.log(`CPU: ${s.aggregatedCPUUsage.toFixed(1)}%`);
  console.log(`Memory: ${s.aggregatedMemoryUsage.toFixed(1)}%`);
  console.log(`Storage: ${s.aggregatedStorageUsage.toFixed(1)}%`);
 
  // Per-node breakdown
  const allNodes = [overview.managerNode, ...overview.workerNodes];
  for (const node of allNodes) {
    const status = node.connectionState === "connected" ? "UP" : "DOWN";
    console.log(`[${status}] ${node.name}: ${node.runningVMCount}/${node.vmCount} VMs`);
  }
 
  // Recent events
  const events = await client.getFleetEvents(5);
  console.log("\n=== Recent Events ===");
  for (const e of events) {
    console.log(`[${e.eventType}] ${e.message}`);
  }
}
 
// Poll every 30 seconds
setInterval(printFleetStatus, 30000);
printFleetStatus();

Python

import time
from ciderstack import FleetClient
 
client = FleetClient("192.168.1.100", api_token="csk_...")
 
def print_fleet_status():
    overview = client.get_fleet_overview()
    stats = overview["overview"]["stats"]
 
    print("=== Fleet Status ===")
    print(f"Nodes: {stats['connectedNodes']}/{stats['totalNodes']}")
    print(f"VMs: {stats['runningVMs']}/{stats['totalVMs']} running")
    print(f"CPU: {stats['aggregatedCPUUsage']:.1f}%")
    print(f"Memory: {stats['aggregatedMemoryUsage']:.1f}%")
 
    # Per-node breakdown
    mgr = overview["overview"]["managerNode"]
    workers = overview["overview"]["workerNodes"]
    for node in [mgr] + workers:
        status = "UP" if node["connectionState"] == "connected" else "DOWN"
        print(f"[{status}] {node['name']}: {node['runningVMCount']}/{node['vmCount']} VMs")
 
    # Recent events
    events = client.get_fleet_events(limit=5)
    print("\n=== Recent Events ===")
    for e in events:
        print(f"[{e['eventType']}] {e['message']}")
 
# Poll every 30 seconds
while True:
    print_fleet_status()
    time.sleep(30)

Snapshot-based testing

Run tests in isolation by snapshotting before and restoring after.

JavaScript

import { FleetClient } from "@ciderstack/fleet-sdk";
 
const client = new FleetClient({ host: "192.168.1.100", apiToken: "csk_..." });
 
async function runTestInIsolation(vmId: string, testCommand: string) {
  // Snapshot the clean state
  const snapshot = await client.createSnapshot(vmId, "pre-test", "Clean state");
 
  try {
    // Run the test
    const result = await client.execCommand(vmId, testCommand, {
      sshUser: "admin",
      sshPassword: "password",
      timeout: 300,
    });
    console.log(`Test ${result.success ? "PASSED" : "FAILED"}`);
    console.log(result.stdout);
    return result.success;
  } finally {
    // Always restore to clean state
    await client.stopVM(vmId);
    await client.restoreSnapshot(vmId, snapshot.id);
    await client.startVM(vmId);
    await client.deleteSnapshot(vmId, snapshot.id);
  }
}

Python

from ciderstack import FleetClient
 
client = FleetClient("192.168.1.100", api_token="csk_...")
 
def run_test_in_isolation(vm_id: str, test_command: str) -> bool:
    """Run a test in snapshot isolation, then restore to clean state."""
    snapshot = client.create_snapshot(vm_id, "pre-test", "Clean state")
 
    try:
        result = client.exec_command(
            vm_id, test_command,
            ssh_user="admin", ssh_password="password", timeout=300,
        )
        print(f"Test {'PASSED' if result.success else 'FAILED'}")
        print(result.stdout)
        return result.success
    finally:
        client.stop_vm(vm_id)
        client.restore_snapshot(vm_id, snapshot.id)
        client.start_vm(vm_id)
        client.delete_snapshot(vm_id, snapshot.id)

Automated snapshot rotation

Keep only the N most recent snapshots per VM.

JavaScript

import { FleetClient } from "@ciderstack/fleet-sdk";
 
const client = new FleetClient({ host: "192.168.1.100", apiToken: "csk_..." });
 
async function rotateSnapshots(vmId: string, maxSnapshots = 5) {
  const snapshots = await client.listSnapshots(vmId);
 
  // Sort newest first
  snapshots.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
 
  // Delete old snapshots beyond the limit
  const toDelete = snapshots.slice(maxSnapshots);
  for (const s of toDelete) {
    console.log(`Deleting: ${s.name} (${s.createdAt.toISOString()})`);
    await client.deleteSnapshot(vmId, s.id);
  }
 
  console.log(`Kept ${Math.min(snapshots.length, maxSnapshots)}, deleted ${toDelete.length}`);
}
 
// Daily snapshot + rotate
async function dailySnapshot(vmId: string) {
  const today = new Date().toISOString().split("T")[0];
  await client.createSnapshot(vmId, `daily-${today}`, "Automated daily snapshot");
  await rotateSnapshots(vmId, 7); // Keep 7 days
}

Python

from ciderstack import FleetClient
from datetime import date
 
client = FleetClient("192.168.1.100", api_token="csk_...")
 
def rotate_snapshots(vm_id: str, max_snapshots: int = 5):
    snapshots = client.list_snapshots(vm_id)
 
    # Sort newest first
    snapshots.sort(key=lambda s: s.created_at, reverse=True)
 
    # Delete old ones
    to_delete = snapshots[max_snapshots:]
    for s in to_delete:
        print(f"Deleting: {s.name} ({s.created_at.isoformat()})")
        client.delete_snapshot(vm_id, s.id)
 
    print(f"Kept {min(len(snapshots), max_snapshots)}, deleted {len(to_delete)}")
 
 
def daily_snapshot(vm_id: str):
    today = date.today().isoformat()
    client.create_snapshot(vm_id, f"daily-{today}", "Automated daily snapshot")
    rotate_snapshots(vm_id, max_snapshots=7)

Template-based provisioning

Create standardized environments from templates.

JavaScript

import { FleetClient } from "@ciderstack/fleet-sdk";
 
const client = new FleetClient({ host: "192.168.1.100", apiToken: "csk_..." });
 
// Provision a dev VM from a template
async function provisionDevVM(developerName: string) {
  const template = await client.getTemplateByName("Developer Workstation");
  if (!template) throw new Error("Template not found");
 
  const vmId = await client.createVM({
    name: `dev-${developerName}`,
    cpuCount: template.defaultCPU,
    memoryMB: template.defaultMemoryMB,
    diskGB: template.defaultDiskGB,
    ociImage: template.id,
  });
 
  // Configure as persistent
  await client.updateVMSettings(vmId, { intent: "persistent" });
 
  // Add shared folder
  await client.addSharedFolder({
    vmId,
    name: "projects",
    hostPath: `/Users/${developerName}/Projects`,
    readOnly: false,
  });
 
  console.log(`Provisioned ${developerName}'s VM: ${vmId}`);
  return vmId;
}
 
// Create a golden template from a configured VM
async function createGoldenTemplate(vmId: string, version: string) {
  await client.stopVM(vmId);
 
  const template = await client.createTemplateFromVM({
    vmId,
    name: `Developer Workstation v${version}`,
    description: `Standard dev environment with Xcode and Homebrew (v${version})`,
    category: "dev",
    keepOriginalVM: true,
  });
 
  console.log(`Template created: ${template.name} (${template.id})`);
  return template;
}

Python

from ciderstack import FleetClient
 
client = FleetClient("192.168.1.100", api_token="csk_...")
 
def provision_dev_vm(developer_name: str) -> str:
    """Provision a dev VM from the standard template."""
    template = client.get_template_by_name("Developer Workstation")
    if not template:
        raise ValueError("Template not found")
 
    vm_id = client.create_vm(
        name=f"dev-{developer_name}",
        cpu_count=template.default_cpu,
        memory_mb=template.default_memory_mb,
        disk_gb=template.default_disk_gb,
        oci_image=template.id,
    )
 
    client.update_vm_settings(vm_id, intent="persistent")
 
    client.add_shared_folder(
        vm_id=vm_id,
        name="projects",
        host_path=f"/Users/{developer_name}/Projects",
        read_only=False,
    )
 
    print(f"Provisioned {developer_name}'s VM: {vm_id}")
    return vm_id
 
 
def create_golden_template(vm_id: str, version: str):
    """Create a new golden template from a configured VM."""
    client.stop_vm(vm_id)
 
    template = client.create_template_from_vm(
        vm_id=vm_id,
        name=f"Developer Workstation v{version}",
        description=f"Standard dev env with Xcode and Homebrew (v{version})",
        category="dev",
        keep_original_vm=True,
    )
 
    print(f"Template created: {template.name} ({template.id})")
    return template

Retry with backoff

Handle transient network errors gracefully.

JavaScript

import { FleetClient, FleetError } from "@ciderstack/fleet-sdk";
 
async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelayMs = 1000,
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;
 
      // Don't retry auth or client errors
      if (error instanceof FleetError && error.response) throw error;
 
      // Network/timeout — retry with exponential backoff
      const delay = baseDelayMs * Math.pow(2, attempt);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error("Unreachable");
}
 
// Usage
const vms = await withRetry(() => client.listVMs());

Python

import time
from ciderstack import FleetClient, FleetError
 
def with_retry(fn, max_retries=3, base_delay=1.0):
    for attempt in range(max_retries + 1):
        try:
            return fn()
        except FleetError as e:
            if e.response:
                raise  # Don't retry auth/client errors
            if attempt == max_retries:
                raise
            time.sleep(base_delay * (2 ** attempt))
        except Exception:
            if attempt == max_retries:
                raise
            time.sleep(base_delay * (2 ** attempt))
 
# Usage
vms = with_retry(lambda: client.list_vms())

Ephemeral VM cleanup

Periodically clean up VMs with expired TTLs.

JavaScript

import { FleetClient } from "@ciderstack/fleet-sdk";
 
const client = new FleetClient({ host: "192.168.1.100", apiToken: "csk_..." });
 
// Run every 5 minutes
setInterval(async () => {
  const cleaned = await client.cleanupVMs();
  if (cleaned > 0) {
    console.log(`Cleaned up ${cleaned} expired VMs`);
  }
}, 5 * 60 * 1000);

Python

import time
from ciderstack import FleetClient
 
client = FleetClient("192.168.1.100", api_token="csk_...")
 
# Run every 5 minutes
while True:
    cleaned = client.cleanup_vms()
    if cleaned > 0:
        print(f"Cleaned up {cleaned} expired VMs")
    time.sleep(300)

See also