fix: Add Python 3.10+ version validation and GitHub Actions Python setup (#180 #167) (#208)

This fixes critical bug where macOS users with default Python 3.9.6 couldn't use Auto-Claude because claude-agent-sdk requires Python 3.10+.

Root Cause:
- Auto-Claude doesn't bundle Python, relies on system Python
- python-detector.ts accepted any Python 3.x without checking minimum version
- macOS ships with Python 3.9.6 by default (incompatible)
- GitHub Actions runners didn't explicitly set Python version

Changes:
1. python-detector.ts:
   - Added getPythonVersion() to extract version from command
   - Added validatePythonVersion() to check if >= 3.10.0
   - Updated findPythonCommand() to skip Python < 3.10 with clear error messages

2. python-env-manager.ts:
   - Import and use findPythonCommand() (already has version validation)
   - Simplified findSystemPython() to use shared validation logic
   - Updated error message from "Python 3.9+" to "Python 3.10+" with download link

3. .github/workflows/release.yml:
   - Added Python 3.11 setup to all 4 build jobs (macOS Intel, macOS ARM64, Windows, Linux)
   - Ensures consistent Python version across all platforms during build

Impact:
- macOS users with Python 3.9 now see clear error with download link
- macOS users with Python 3.10+ work normally
- CI/CD builds use consistent Python 3.11
- Prevents "ModuleNotFoundError: dotenv" and dependency install failures

Fixes #180, #167

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Fernando Possebon
2025-12-23 12:34:02 -03:00
committed by GitHub
parent e3eec68aab
commit f168bdc3ac
3 changed files with 117 additions and 58 deletions
+20
View File
@@ -20,6 +20,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -84,6 +89,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -147,6 +157,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -188,6 +203,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
+73 -7
View File
@@ -18,17 +18,18 @@ export function findPythonCommand(): string | null {
for (const cmd of candidates) {
try {
const version = execSync(`${cmd} --version`, {
stdio: 'pipe',
timeout: 5000,
windowsHide: true
}).toString();
if (version.includes('Python 3')) {
// Validate version meets minimum requirement (Python 3.10+)
const validation = validatePythonVersion(cmd);
if (validation.valid) {
console.log(`[Python] Found valid Python: ${cmd} (${validation.version})`);
return cmd;
} else {
console.warn(`[Python] ${cmd} version too old: ${validation.message}`);
continue;
}
} catch {
// Command not found or errored, try next
console.warn(`[Python] Command not found or errored: ${cmd}`);
continue;
}
}
@@ -37,6 +38,71 @@ export function findPythonCommand(): string | null {
return isWindows ? 'python' : 'python3';
}
/**
* Extract Python version from a command.
*
* @param pythonCmd - The Python command to check (e.g., "python3", "py -3")
* @returns The version string (e.g., "3.10.5") or null if unable to detect
*/
function getPythonVersion(pythonCmd: string): string | null {
try {
const version = execSync(`${pythonCmd} --version`, {
stdio: 'pipe',
timeout: 5000,
windowsHide: true
}).toString().trim();
// Extract version number from "Python 3.10.5" format
const match = version.match(/Python (\d+\.\d+\.\d+)/);
return match ? match[1] : null;
} catch {
return null;
}
}
/**
* Validate that a Python command meets minimum version requirements.
*
* @param pythonCmd - The Python command to validate
* @returns Validation result with status, version, and message
*/
function validatePythonVersion(pythonCmd: string): {
valid: boolean;
version?: string;
message: string;
} {
const MINIMUM_VERSION = '3.10.0';
const versionStr = getPythonVersion(pythonCmd);
if (!versionStr) {
return {
valid: false,
message: 'Unable to detect Python version'
};
}
// Parse version numbers for comparison
const [major, minor] = versionStr.split('.').map(Number);
const [reqMajor, reqMinor] = MINIMUM_VERSION.split('.').map(Number);
const meetsRequirement =
major > reqMajor || (major === reqMajor && minor >= reqMinor);
if (!meetsRequirement) {
return {
valid: false,
version: versionStr,
message: `Python ${versionStr} is too old. Requires Python ${MINIMUM_VERSION}+ (claude-agent-sdk requirement)`
};
}
return {
valid: true,
version: versionStr,
message: `Python ${versionStr} meets requirements`
};
}
/**
* Get the default Python command for the current platform.
* This is a synchronous fallback that doesn't test if Python actually exists.
+24 -51
View File
@@ -3,6 +3,7 @@ import { existsSync } from 'fs';
import path from 'path';
import { EventEmitter } from 'events';
import { app } from 'electron';
import { findPythonCommand } from './python-detector';
export interface PythonEnvStatus {
ready: boolean;
@@ -96,61 +97,29 @@ export class PythonEnvManager extends EventEmitter {
}
/**
* Find system Python3
* Find system Python 3.10+
* Uses the shared python-detector logic which validates version requirements.
*/
private findSystemPython(): string | null {
const isWindows = process.platform === 'win32';
// Windows candidates - py launcher is handled specially
// Unix candidates - try python3 first, then python
const candidates = isWindows
? ['python', 'python3']
: ['python3', 'python'];
// On Windows, try the py launcher first (most reliable)
if (isWindows) {
try {
// py -3 runs Python 3, verify it works
const version = execSync('py -3 --version', {
stdio: 'pipe',
timeout: 5000
}).toString();
if (version.includes('Python 3')) {
// Get the actual executable path
const pythonPath = execSync('py -3 -c "import sys; print(sys.executable)"', {
stdio: 'pipe',
timeout: 5000
}).toString().trim();
return pythonPath;
}
} catch {
// py launcher not available, continue with other candidates
}
const pythonCmd = findPythonCommand();
if (!pythonCmd) {
return null;
}
for (const cmd of candidates) {
try {
const version = execSync(`${cmd} --version`, {
stdio: 'pipe',
timeout: 5000
}).toString();
if (version.includes('Python 3')) {
// Get the actual path
// On Windows, use Python itself to get the path
// On Unix, use 'which'
const pathCmd = isWindows
? `${cmd} -c "import sys; print(sys.executable)"`
: `which ${cmd}`;
const pythonPath = execSync(pathCmd, { stdio: 'pipe', timeout: 5000 })
.toString()
.trim();
return pythonPath;
}
} catch {
continue;
}
try {
// Get the actual executable path from the command
// For commands like "py -3", we need to resolve to the actual executable
const pythonPath = execSync(`${pythonCmd} -c "import sys; print(sys.executable)"`, {
stdio: 'pipe',
timeout: 5000
}).toString().trim();
console.log(`[PythonEnvManager] Found Python at: ${pythonPath}`);
return pythonPath;
} catch (err) {
console.error(`[PythonEnvManager] Failed to get Python path for ${pythonCmd}:`, err);
return null;
}
return null;
}
/**
@@ -161,7 +130,11 @@ export class PythonEnvManager extends EventEmitter {
const systemPython = this.findSystemPython();
if (!systemPython) {
this.emit('error', 'Python 3 not found. Please install Python 3.9+');
this.emit(
'error',
'Python 3.10+ not found. Please install Python 3.10 or higher (required by claude-agent-sdk).\n\n' +
'Download: https://www.python.org/downloads/'
);
return false;
}