From 26146429654323fd26c839413923da450752028b Mon Sep 17 00:00:00 2001 From: Jason Robert Date: Wed, 25 Mar 2026 15:55:56 -0400 Subject: [PATCH 1/3] feat: install scripts with constrained dependency pinning - Add install.sh (macOS/Linux) and install.ps1 (Windows) installer scripts that auto-detect uv, fetch the latest release, download constraints.txt with SHA-256 verification, and run uv tool install with pinned deps - Update release workflow to generate constraints.txt and constraints.txt.sha256 as release assets via uv export - Update conductor update to download and verify constraints file before upgrading, with graceful fallback for older releases - Update README with quick install instructions using aka.ms short URLs --- .github/workflows/release.yml | 6 ++ README.md | 32 +++++-- install.ps1 | 123 +++++++++++++++++++++++++ install.sh | 164 ++++++++++++++++++++++++++++++++++ src/conductor/cli/update.py | 108 ++++++++++++++++++---- 5 files changed, 410 insertions(+), 23 deletions(-) create mode 100644 install.ps1 create mode 100755 install.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e7d5d2..2c9ca61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,6 +144,12 @@ jobs: ls -la dist/ uv run python -m zipfile -l dist/*.whl + - name: Generate constraints file + run: | + uv export --no-hashes --frozen --no-emit-project --no-annotate \ + -o dist/constraints.txt + sha256sum dist/constraints.txt > dist/constraints.txt.sha256 + - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index cc6895e..b7d4deb 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,31 @@ Conductor provides the patterns that work: evaluator-optimizer loops for iterati ## Installation -### Using uv (Recommended) +### Quick Install (Recommended) +**macOS / Linux:** ```bash -# Install from GitHub (--locked ensures reproducible dependency versions) -uv tool install --locked git+https://github.com/microsoft/conductor.git +curl -sSfL https://aka.ms/conductor/install.sh | sh +``` + +**Windows (PowerShell):** +```powershell +irm https://aka.ms/conductor/install.ps1 | iex +``` + +The installer checks for [uv](https://docs.astral.sh/uv/) (installs it if missing), fetches the latest release with pinned dependencies, and verifies integrity via SHA-256 checksum. + +### Updating + +```bash +conductor update +``` + +### Manual Install + +```bash +# Install from GitHub +uv tool install git+https://github.com/microsoft/conductor.git # Run the CLI conductor run workflow.yaml @@ -38,9 +58,9 @@ conductor run workflow.yaml uvx --from git+https://github.com/microsoft/conductor.git conductor run workflow.yaml # Install a specific branch, tag, or commit -uv tool install --locked git+https://github.com/microsoft/conductor.git@branch-name -uv tool install --locked git+https://github.com/microsoft/conductor.git@v1.0.0 -uv tool install --locked git+https://github.com/microsoft/conductor.git@abc1234 +uv tool install git+https://github.com/microsoft/conductor.git@branch-name +uv tool install git+https://github.com/microsoft/conductor.git@v1.0.0 +uv tool install git+https://github.com/microsoft/conductor.git@abc1234 ``` ### Using pipx diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..6bdf9f1 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,123 @@ +# Conductor installer for Windows (PowerShell) +# Usage: irm https://aka.ms/conductor/install.ps1 | iex +# +# This script: +# 1. Checks for uv (installs it if missing) +# 2. Fetches the latest Conductor release from GitHub +# 3. Downloads and verifies the constraints file (SHA-256) +# 4. Installs Conductor via uv tool install with pinned dependencies + +$ErrorActionPreference = 'Stop' + +$Repo = 'microsoft/conductor' +$GitHubApi = "https://api.github.com/repos/$Repo/releases/latest" +$GitHubDL = "https://github.com/$Repo/releases/download" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +function Write-Info { param([string]$Msg) Write-Host " → $Msg" -ForegroundColor Cyan } +function Write-Ok { param([string]$Msg) Write-Host " ✓ $Msg" -ForegroundColor Green } +function Write-Err { param([string]$Msg) Write-Host " ✗ $Msg" -ForegroundColor Red; exit 1 } + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +Write-Host "`nConductor Installer`n" -ForegroundColor White -NoNewline +Write-Host "" + +# --- uv --- +$uvCmd = Get-Command uv -ErrorAction SilentlyContinue +if (-not $uvCmd) { + Write-Info "uv not found — installing…" + irm https://astral.sh/uv/install.ps1 | iex + # Refresh PATH so uv is available + $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'User') + ';' + + [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + $uvCmd = Get-Command uv -ErrorAction SilentlyContinue + if (-not $uvCmd) { + Write-Err "uv installation succeeded but 'uv' is not on PATH. Please restart your terminal and retry." + } + Write-Ok "uv installed" +} else { + Write-Ok "uv found at $($uvCmd.Source)" +} + +# --- Detect latest release --- +Write-Info "Fetching latest release…" +$headers = @{ Accept = 'application/vnd.github+json' } +$release = Invoke-RestMethod -Uri $GitHubApi -Headers $headers +$tagName = $release.tag_name + +if (-not $tagName) { + Write-Err "Could not determine latest release tag from GitHub API." +} + +Write-Ok "Latest release: $tagName" + +# --- Check existing installation --- +$existingConductor = Get-Command conductor -ErrorAction SilentlyContinue +if ($existingConductor) { + $currentVersion = $null + try { + $versionOutput = conductor --version 2>$null + if ($versionOutput -match '(\d+\.\d+\.\d+[^ ]*)') { + $currentVersion = $Matches[1] + } + } catch { } + + if ($currentVersion) { + $latestVersion = $tagName -replace '^v', '' + if ($currentVersion -eq $latestVersion) { + Write-Ok "Conductor v$currentVersion is already installed and up to date." + Write-Host "" + Write-Host " Run 'conductor --help' to get started." + Write-Host "" + exit 0 + } + Write-Info "Upgrading Conductor: v$currentVersion → $tagName" + } +} + +# --- Download constraints + checksum to temp directory --- +$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "conductor-install-$([guid]::NewGuid().ToString('N').Substring(0,8))" +New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null + +try { + Write-Info "Downloading constraints…" + $constraintsUrl = "$GitHubDL/$tagName/constraints.txt" + $checksumUrl = "$GitHubDL/$tagName/constraints.txt.sha256" + $constraintsFile = Join-Path $tmpDir 'constraints.txt' + $checksumFile = Join-Path $tmpDir 'constraints.txt.sha256' + + Invoke-WebRequest -Uri $constraintsUrl -OutFile $constraintsFile -UseBasicParsing + Invoke-WebRequest -Uri $checksumUrl -OutFile $checksumFile -UseBasicParsing + + # --- Verify checksum --- + Write-Info "Verifying checksum…" + $expectedHash = (Get-Content $checksumFile -Raw).Trim().Split(' ')[0] + $actualHash = (Get-FileHash -Path $constraintsFile -Algorithm SHA256).Hash.ToLower() + + if ($actualHash -ne $expectedHash) { + Write-Err "Checksum verification failed for constraints.txt (expected $expectedHash, got $actualHash)" + } + Write-Ok "Checksum verified" + + # --- Install --- + Write-Info "Installing Conductor $tagName…" + uv tool install --force "git+https://github.com/$Repo.git@$tagName" -c $constraintsFile + + if ($LASTEXITCODE -ne 0) { + Write-Err "uv tool install failed with exit code $LASTEXITCODE" + } + + Write-Ok "Conductor $tagName installed" + Write-Host "" + Write-Host " Run 'conductor --help' to get started." + Write-Host " Run 'conductor update' to check for future updates." + Write-Host "" +} finally { + Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..012e80d --- /dev/null +++ b/install.sh @@ -0,0 +1,164 @@ +#!/bin/sh +# Conductor installer for macOS and Linux +# Usage: curl -sSfL https://aka.ms/conductor/install.sh | sh +# +# This script: +# 1. Checks for uv (installs it if missing) +# 2. Fetches the latest Conductor release from GitHub +# 3. Downloads and verifies the constraints file (SHA-256) +# 4. Installs Conductor via uv tool install with pinned dependencies + +set -eu + +REPO="microsoft/conductor" +GITHUB_API="https://api.github.com/repos/${REPO}/releases/latest" +GITHUB_DL="https://github.com/${REPO}/releases/download" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +info() { + printf ' \033[1;34m→\033[0m %s\n' "$1" +} + +success() { + printf ' \033[1;32m✓\033[0m %s\n' "$1" +} + +error() { + printf ' \033[1;31m✗\033[0m %s\n' "$1" >&2 + exit 1 +} + +need_cmd() { + if ! command -v "$1" > /dev/null 2>&1; then + return 1 + fi +} + +# --------------------------------------------------------------------------- +# Download helper — works with curl or wget +# --------------------------------------------------------------------------- + +download() { + url="$1" + dest="$2" + + if need_cmd curl; then + curl -sSfL -o "$dest" "$url" + elif need_cmd wget; then + wget -qO "$dest" "$url" + else + error "Neither curl nor wget found. Please install one and retry." + fi +} + +download_stdout() { + url="$1" + + if need_cmd curl; then + curl -sSfL "$url" + elif need_cmd wget; then + wget -qO- "$url" + else + error "Neither curl nor wget found. Please install one and retry." + fi +} + +# --------------------------------------------------------------------------- +# SHA-256 verification — works on macOS and Linux +# --------------------------------------------------------------------------- + +verify_checksum() { + file="$1" + expected="$2" + + if need_cmd sha256sum; then + actual=$(sha256sum "$file" | cut -d' ' -f1) + elif need_cmd shasum; then + actual=$(shasum -a 256 "$file" | cut -d' ' -f1) + else + info "Warning: cannot verify checksum (sha256sum/shasum not found), skipping" + return 0 + fi + + if [ "$actual" != "$expected" ]; then + error "Checksum verification failed for constraints.txt (expected ${expected}, got ${actual})" + fi +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +main() { + printf '\n\033[1mConductor Installer\033[0m\n\n' + + # --- uv --- + if ! need_cmd uv; then + info "uv not found — installing…" + curl -sSfL https://astral.sh/uv/install.sh | sh + # Source the env so uv is on PATH for the rest of this script + if [ -f "$HOME/.local/bin/env" ]; then + . "$HOME/.local/bin/env" + fi + export PATH="$HOME/.local/bin:$PATH" + if ! need_cmd uv; then + error "uv installation succeeded but 'uv' is not on PATH. Please add ~/.local/bin to your PATH and retry." + fi + success "uv installed" + else + success "uv found at $(command -v uv)" + fi + + # --- Detect latest release --- + info "Fetching latest release…" + release_json=$(download_stdout "$GITHUB_API") + tag_name=$(printf '%s' "$release_json" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4) + + if [ -z "$tag_name" ]; then + error "Could not determine latest release tag from GitHub API." + fi + + success "Latest release: ${tag_name}" + + # --- Check existing installation --- + if need_cmd conductor; then + current_version=$(conductor --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+[^ ]*' | head -1) + if [ -n "$current_version" ]; then + latest_version=$(printf '%s' "$tag_name" | sed 's/^v//') + if [ "$current_version" = "$latest_version" ]; then + success "Conductor v${current_version} is already installed and up to date." + printf '\n Run \033[1mconductor --help\033[0m to get started.\n\n' + return 0 + fi + info "Upgrading Conductor: v${current_version} → ${tag_name}" + fi + fi + + # --- Download constraints + checksum --- + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"' EXIT + + info "Downloading constraints…" + download "${GITHUB_DL}/${tag_name}/constraints.txt" "${tmpdir}/constraints.txt" + download "${GITHUB_DL}/${tag_name}/constraints.txt.sha256" "${tmpdir}/constraints.txt.sha256" + + # --- Verify checksum --- + info "Verifying checksum…" + expected_hash=$(cut -d' ' -f1 "${tmpdir}/constraints.txt.sha256") + verify_checksum "${tmpdir}/constraints.txt" "$expected_hash" + success "Checksum verified" + + # --- Install --- + info "Installing Conductor ${tag_name}…" + uv tool install --force "git+https://github.com/${REPO}.git@${tag_name}" \ + -c "${tmpdir}/constraints.txt" + + success "Conductor ${tag_name} installed" + printf '\n Run \033[1mconductor --help\033[0m to get started.\n' + printf ' Run \033[1mconductor update\033[0m to check for future updates.\n\n' +} + +main diff --git a/src/conductor/cli/update.py b/src/conductor/cli/update.py index c0b20fd..45b6dff 100644 --- a/src/conductor/cli/update.py +++ b/src/conductor/cli/update.py @@ -14,11 +14,13 @@ from __future__ import annotations import contextlib +import hashlib import json import logging import shutil import subprocess import sys +import tempfile import urllib.error import urllib.request from datetime import UTC, datetime @@ -35,6 +37,7 @@ _API_URL = "https://api.github.com/repos/microsoft/conductor/releases/latest" _FETCH_TIMEOUT_SECONDS = 2 _REPO_GIT_URL = "https://github.com/microsoft/conductor.git" +_RELEASE_DL_URL = "https://github.com/microsoft/conductor/releases/download" # --------------------------------------------------------------------------- @@ -293,6 +296,9 @@ def run_update(console: Console) -> None: This always bypasses the cache and fetches from the network. On success the cache file is deleted so the next invocation will re-check cleanly. + The upgrade pins transitive dependencies using a constraints file + published with each GitHub Release, verified via SHA-256 checksum. + Args: console: Rich console for output. """ @@ -313,7 +319,13 @@ def run_update(console: Console) -> None: console.print(f"Upgrading Conductor: v{current} → v{version}") install_url = f"git+{_REPO_GIT_URL}@{tag_name}" - cmd = ["uv", "tool", "install", "--force", "--locked", install_url] + + # Download constraints file and verify checksum + constraints_path = _download_constraints(tag_name, console) + + cmd = ["uv", "tool", "install", "--force", install_url] + if constraints_path: + cmd.extend(["-c", str(constraints_path)]) # On Windows, rename our exe out of the way so uv can write the new one. # Windows locks running executables but allows renaming them. @@ -331,19 +343,81 @@ def run_update(console: Console) -> None: except OSError: old_exe = None # rename failed; proceed anyway, uv will report the error - proc = subprocess.run(cmd, capture_output=True, text=True) # noqa: S603 - - if proc.returncode == 0: - console.print(f"[green]Successfully upgraded to v{version}[/green]") - cache_path = get_cache_path() - cache_path.unlink(missing_ok=True) - else: - console.print(f"[bold red]Upgrade failed[/bold red] (exit code {proc.returncode})") - if proc.stderr: - console.print(f"[dim]{proc.stderr.strip()}[/dim]") - # On Windows, restore the original exe if uv failed to write a new one - if old_exe and old_exe.exists(): - exe_path = old_exe.with_suffix("") # .exe.old → .exe - if not exe_path.exists(): - with contextlib.suppress(OSError): - old_exe.rename(exe_path) + try: + proc = subprocess.run(cmd, capture_output=True, text=True) # noqa: S603 + + if proc.returncode == 0: + console.print(f"[green]Successfully upgraded to v{version}[/green]") + cache_path = get_cache_path() + cache_path.unlink(missing_ok=True) + else: + console.print(f"[bold red]Upgrade failed[/bold red] (exit code {proc.returncode})") + if proc.stderr: + console.print(f"[dim]{proc.stderr.strip()}[/dim]") + # On Windows, restore the original exe if uv failed to write a new one + if old_exe and old_exe.exists(): + exe_path = old_exe.with_suffix("") # .exe.old → .exe + if not exe_path.exists(): + with contextlib.suppress(OSError): + old_exe.rename(exe_path) + finally: + # Clean up temp constraints file + if constraints_path: + with contextlib.suppress(OSError): + constraints_path.unlink() + constraints_path.parent.rmdir() + + +def _download_constraints(tag_name: str, console: Console) -> Path | None: + """Download and verify the constraints file for a release. + + Args: + tag_name: The release tag (e.g. ``v0.3.0``). + console: Rich console for status output. + + Returns: + Path to the downloaded constraints file, or ``None`` if unavailable. + """ + constraints_url = f"{_RELEASE_DL_URL}/{tag_name}/constraints.txt" + checksum_url = f"{_RELEASE_DL_URL}/{tag_name}/constraints.txt.sha256" + + tmpdir = Path(tempfile.mkdtemp(prefix="conductor-update-")) + constraints_path = tmpdir / "constraints.txt" + + try: + # Download constraints file + req = urllib.request.Request(constraints_url) + with urllib.request.urlopen(req, timeout=10) as resp: # noqa: S310 + constraints_path.write_bytes(resp.read()) + + # Download checksum + req = urllib.request.Request(checksum_url) + with urllib.request.urlopen(req, timeout=10) as resp: # noqa: S310 + checksum_content = resp.read().decode().strip() + expected_hash = checksum_content.split()[0] + + # Verify + actual_hash = hashlib.sha256(constraints_path.read_bytes()).hexdigest() + if actual_hash != expected_hash: + console.print( + "[bold red]Error:[/bold red] Constraints file checksum mismatch — " + "skipping constraints." + ) + with contextlib.suppress(OSError): + constraints_path.unlink() + tmpdir.rmdir() + return None + + console.print("[dim]Constraints verified ✓[/dim]") + return constraints_path + + except Exception: # noqa: BLE001 + logger.debug("Failed to download constraints file", exc_info=True) + console.print( + "[dim]Constraints file not available for this release," + " installing without.[/dim]" + ) + with contextlib.suppress(OSError): + constraints_path.unlink() + tmpdir.rmdir() + return None From 7e7da8218ce27efda43c3bd726e572f3a0266c55 Mon Sep 17 00:00:00 2001 From: Jason Robert Date: Wed, 25 Mar 2026 16:00:16 -0400 Subject: [PATCH 2/3] docs: update install references for constrained dependency pinning --- .claude/skills/conductor/references/execution.md | 2 +- AGENTS.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/conductor/references/execution.md b/.claude/skills/conductor/references/execution.md index 88e78e9..6db4034 100644 --- a/.claude/skills/conductor/references/execution.md +++ b/.claude/skills/conductor/references/execution.md @@ -115,7 +115,7 @@ conductor update The command: 1. Fetches the latest release from the GitHub Releases API 2. Compares the remote version with the locally installed version -3. If a newer version is available, runs `uv tool install --force --locked git+https://github.com/microsoft/conductor.git@v{version}` to upgrade +3. If a newer version is available, runs `uv tool install --force git+https://github.com/microsoft/conductor.git@v{version}` to upgrade 4. Clears the update-check cache on success so the next invocation re-checks cleanly If already up to date, prints a confirmation message and exits. diff --git a/AGENTS.md b/AGENTS.md index ec7d848..d9618ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ make validate-examples # validate all examples - `run.py` - Workflow execution command with verbose logging helpers - `bg_runner.py` - Background process forking for `--web-bg` mode - `pid.py` - PID file utilities for tracking/stopping background processes - - `update.py` - Update check, version comparison, and self-upgrade via `uv tool install --locked` + - `update.py` - Update check, version comparison, and self-upgrade via `uv tool install` - **config/**: YAML loading and Pydantic schema validation - `schema.py` - Pydantic models for all workflow YAML structures (WorkflowConfig, AgentDef, ParallelGroup, ForEachDef, etc.) From 0c74c0655b20ca606a9dd7d752fc7972d9e4ddfe Mon Sep 17 00:00:00 2001 From: Jason Robert Date: Wed, 25 Mar 2026 16:01:46 -0400 Subject: [PATCH 3/3] style: fix ruff formatting in update.py --- src/conductor/cli/update.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/conductor/cli/update.py b/src/conductor/cli/update.py index 45b6dff..552439e 100644 --- a/src/conductor/cli/update.py +++ b/src/conductor/cli/update.py @@ -414,8 +414,7 @@ def _download_constraints(tag_name: str, console: Console) -> Path | None: except Exception: # noqa: BLE001 logger.debug("Failed to download constraints file", exc_info=True) console.print( - "[dim]Constraints file not available for this release," - " installing without.[/dim]" + "[dim]Constraints file not available for this release, installing without.[/dim]" ) with contextlib.suppress(OSError): constraints_path.unlink()