Compare commits

...

5 Commits

Author SHA1 Message Date
openhands
944530a72d Fix PyInstaller --target-arch issue with spec files
PyInstaller doesn't allow --target-arch flag when using .spec files.
Instead, modify the spec file directly to set target_arch parameter.

- Dynamically modify spec file to set target_arch for macOS builds
- Restore original spec file after build completes
- Remove --target-arch command line argument usage

This fixes the CI build failure for macOS ARM64 architecture.
2025-10-10 16:54:49 +00:00
openhands
09de78d426 Add multi-architecture support for CLI binary builds
- Expand CI matrix to build for x86_64 and ARM64 on both Linux and macOS
- Add --target-arch parameter to build.py for macOS cross-compilation
- Update artifact naming to include platform and architecture
- Enhance release asset preparation for multi-arch binaries

Fixes #11320

Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-10 16:48:06 +00:00
Rohit Malhotra
c034cc5dfb Refactor: move helper function to avoid circular imports (#11310)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-10 12:40:03 -04:00
Hiep Le
9bd02440b0 fix(frontend): some user interface elements are overlapping with the Create API Key modal (#11301) 2025-10-10 22:54:10 +07:00
Rohit Malhotra
c9d8782566 V1(CLI): Release (#11317) 2025-10-10 15:25:19 +00:00
14 changed files with 120 additions and 147 deletions

View File

@@ -12,6 +12,9 @@ on:
paths:
- "openhands-cli/**"
permissions:
contents: write # needed to create releases or upload assets
# Cancel previous runs if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
@@ -22,7 +25,23 @@ jobs:
name: Build and test binary executable
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
include:
# Linux x86_64
- os: ubuntu-latest
arch: x86_64
platform: linux
# Linux ARM64
- os: ubuntu-latest-arm64
arch: arm64
platform: linux
# macOS x86_64 (Intel)
- os: macos-13
arch: x86_64
platform: macos
# macOS ARM64 (Apple Silicon)
- os: macos-latest
arch: arm64
platform: macos
runs-on: ${{ matrix.os }}
steps:
@@ -49,7 +68,14 @@ jobs:
- name: Build binary executable
working-directory: openhands-cli
run: |
./build.sh --install-pyinstaller | tee output.log
# Set build arguments based on platform and architecture
BUILD_ARGS="--install-pyinstaller"
if [ "${{ matrix.platform }}" = "macos" ]; then
BUILD_ARGS="$BUILD_ARGS --target-arch ${{ matrix.arch }}"
fi
echo "🔨 Building with args: $BUILD_ARGS"
./build.sh $BUILD_ARGS | tee output.log
echo "Full output:"
cat output.log
@@ -64,7 +90,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: openhands-cli-${{ matrix.os }}
name: openhands-cli-${{ matrix.platform }}-${{ matrix.arch }}
path: openhands-cli/dist/openhands*
retention-days: 30
@@ -85,13 +111,24 @@ jobs:
- name: Prepare release assets
run: |
mkdir -p release-assets
# Rename binaries to include OS in filename
if [ -f artifacts/openhands-cli-ubuntu-latest/openhands ]; then
cp artifacts/openhands-cli-ubuntu-latest/openhands release-assets/openhands-linux
fi
if [ -f artifacts/openhands-cli-macos-latest/openhands ]; then
cp artifacts/openhands-cli-macos-latest/openhands release-assets/openhands-macos
fi
# Rename binaries to include platform and architecture in filename
for artifact_dir in artifacts/openhands-cli-*; do
if [ -d "$artifact_dir" ]; then
# Extract platform and arch from directory name
# Format: openhands-cli-{platform}-{arch}
dir_name=$(basename "$artifact_dir")
platform_arch=${dir_name#openhands-cli-}
if [ -f "$artifact_dir/openhands" ]; then
cp "$artifact_dir/openhands" "release-assets/openhands-$platform_arch"
echo "✅ Copied $artifact_dir/openhands to release-assets/openhands-$platform_arch"
else
echo "⚠️ No openhands binary found in $artifact_dir"
fi
fi
done
echo "📁 Release assets prepared:"
ls -la release-assets/
- name: Create GitHub Release
@@ -101,4 +138,4 @@ jobs:
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.CLI_RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,107 +0,0 @@
# Workflow that builds and tests the CLI binary executable
name: CLI - Build and Test Binary
# Run on pushes to main branch and CLI tags, and on pull requests when CLI files change
on:
push:
branches:
- main
tags:
- "*-cli"
pull_request:
paths:
- "openhands-cli/**"
permissions:
contents: write # needed to create releases or upload assets
# Cancel previous runs if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
build-and-test-binary:
name: Build and test binary executable
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: openhands-cli
run: |
uv sync
- name: Build binary executable
working-directory: openhands-cli
run: |
./build.sh --install-pyinstaller | tee output.log
echo "Full output:"
cat output.log
if grep -q "❌" output.log; then
echo "❌ Found failure marker in output"
exit 1
fi
echo "✅ Build & test finished without ❌ markers"
- name: Upload binary artifact (for releases only)
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: openhands-cli-${{ matrix.os }}
path: openhands-cli/dist/openhands*
retention-days: 30
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: build-and-test-binary
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release assets
run: |
mkdir -p release-assets
# Rename binaries to include OS in filename
if [ -f artifacts/openhands-cli-ubuntu-latest/openhands ]; then
cp artifacts/openhands-cli-ubuntu-latest/openhands release-assets/openhands-linux
fi
if [ -f artifacts/openhands-cli-macos-latest/openhands ]; then
cp artifacts/openhands-cli-macos-latest/openhands release-assets/openhands-macos
fi
ls -la release-assets/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release-assets/*
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,14 +1,17 @@
# Publishes the OpenHands PyPi package
name: Publish PyPi Package
on:
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
description: "What are you publishing?"
required: true
default: ''
type: choice
options:
- app server
- cli
default: app server
push:
tags:
- "*"
@@ -16,8 +19,10 @@ on:
jobs:
release:
runs-on: blacksmith-4vcpu-ubuntu-2204
# Only run for tags that don't contain '-cli'
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli')
# Run when manually dispatched for "app server" OR for tag pushes that don't contain '-cli'
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'app server')
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli'))
steps:
- uses: actions/checkout@v4
- uses: useblacksmith/setup-python@v6
@@ -38,8 +43,10 @@ jobs:
release-cli:
name: Publish CLI to PyPI
runs-on: ubuntu-latest
# Only run for tags that contain '-cli'
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli')
# Run when manually dispatched for "cli" OR for tag pushes that contain '-cli'
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'cli')
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -64,4 +71,4 @@ jobs:
- name: Publish CLI to PyPI
working-directory: openhands-cli
run: |
uv publish --token ${{ secrets.PYPI_TOKEN }}
uv publish --token ${{ secrets.PYPI_TOKEN_OPENHANDS }}

View File

@@ -13,7 +13,7 @@ from integrations.solvability.models.report import SolvabilityReport
from integrations.solvability.models.summary import SolvabilitySummary
from integrations.utils import ENABLE_SOLVABILITY_ANALYSIS
from pydantic import ValidationError
from server.auth.token_manager import get_config
from server.config import get_config
from storage.database import session_maker
from storage.saas_settings_store import SaasSettingsStore

View File

@@ -19,7 +19,8 @@ from integrations.utils import (
from jinja2 import Environment
from pydantic.dataclasses import dataclass
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
from server.auth.token_manager import TokenManager, get_config
from server.auth.token_manager import TokenManager
from server.config import get_config
from storage.database import session_maker
from storage.proactive_conversation_store import ProactiveConversationStore
from storage.saas_secrets_store import SaasSecretsStore

View File

@@ -4,7 +4,8 @@ from integrations.models import Message
from integrations.types import ResolverViewInterface, UserData
from integrations.utils import HOST, get_oh_labels, has_exact_mention
from jinja2 import Environment
from server.auth.token_manager import TokenManager, get_config
from server.auth.token_manager import TokenManager
from server.config import get_config
from storage.database import session_maker
from storage.saas_secrets_store import SaasSecretsStore

View File

@@ -13,7 +13,8 @@ from server.auth.auth_error import (
ExpiredError,
NoCredentialsError,
)
from server.auth.token_manager import TokenManager, get_config
from server.auth.token_manager import TokenManager
from server.config import get_config
from server.logger import logger
from server.rate_limit import RateLimiter, create_redis_rate_limiter
from storage.api_key_store import ApiKeyStore

View File

@@ -26,6 +26,7 @@ from server.auth.constants import (
KEYCLOAK_SERVER_URL_EXT,
)
from server.auth.keycloak_manager import get_keycloak_admin, get_keycloak_openid
from server.config import get_config
from server.logger import logger
from sqlalchemy import String as SQLString
from sqlalchemy import type_coerce
@@ -35,19 +36,8 @@ from storage.github_app_installation import GithubAppInstallation
from storage.offline_token_store import OfflineTokenStore
from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt
from openhands.core.config import load_openhands_config
from openhands.integrations.service_types import ProviderType
# Create a function to get config to avoid circular imports
_config = None
def get_config():
global _config
if _config is None:
_config = load_openhands_config()
return _config
def _before_sleep_callback(retry_state: RetryCallState) -> None:
logger.info(f'Retry attempt {retry_state.attempt_number} for Keycloak operation')

View File

@@ -19,10 +19,21 @@ from server.auth.constants import (
GITLAB_APP_CLIENT_ID,
)
from openhands.core.config.utils import load_openhands_config
from openhands.integrations.service_types import ProviderType
from openhands.server.config.server_config import ServerConfig
from openhands.server.types import AppMode
# Create a function to get config to avoid circular imports
_config = None
def get_config():
global _config
if _config is None:
_config = load_openhands_config()
return _config
def sign_token(payload: dict[str, object], jwt_secret: str, algorithm='HS256') -> str:
"""Signs a JWT token."""

View File

@@ -20,7 +20,7 @@ def token_store(session_maker, mock_config):
@pytest.fixture
def token_manager():
with patch('server.auth.token_manager.get_config') as mock_get_config:
with patch('server.config.get_config') as mock_get_config:
mock_config = mock_get_config.return_value
mock_config.jwt_secret.get_secret_value.return_value = 'test_secret'
return TokenManager(external=False)

View File

@@ -8,7 +8,7 @@ from openhands.integrations.service_types import ProviderType
@pytest.fixture
def token_manager():
with patch('server.auth.token_manager.get_config') as mock_get_config:
with patch('server.config.get_config') as mock_get_config:
mock_config = mock_get_config.return_value
mock_config.jwt_secret.get_secret_value.return_value = 'test_secret'
return TokenManager(external=False)

View File

@@ -20,7 +20,7 @@ export function ModalBackdrop({ children, onClose }: ModalBackdropProps) {
};
return (
<div className="fixed inset-0 flex items-center justify-center z-20">
<div className="fixed inset-0 flex items-center justify-center z-60">
<div
onClick={handleClick}
className="fixed inset-0 bg-black opacity-60"

View File

@@ -72,6 +72,7 @@ def check_pyinstaller() -> bool:
def build_executable(
spec_file: str = 'openhands.spec',
clean: bool = True,
target_arch: str = None,
) -> bool:
"""Build the executable using PyInstaller."""
if clean:
@@ -83,8 +84,27 @@ def build_executable(
print(f'🔨 Building executable using {spec_file}...')
# Handle target architecture for macOS by modifying the spec file
original_spec_content = None
if target_arch and sys.platform == 'darwin':
print(f'🎯 Building for macOS target architecture: {target_arch}')
# Read the original spec file
with open(spec_file, 'r') as f:
original_spec_content = f.read()
# Replace target_arch=None with target_arch='<arch>'
modified_spec_content = original_spec_content.replace(
'target_arch=None',
f"target_arch='{target_arch}'"
)
# Write the modified spec file
with open(spec_file, 'w') as f:
f.write(modified_spec_content)
try:
# Run PyInstaller with uv
# Run PyInstaller with uv (no --target-arch flag needed with spec file)
cmd = ['uv', 'run', 'pyinstaller', spec_file, '--clean']
print(f'Running: {" ".join(cmd)}')
@@ -113,6 +133,12 @@ def build_executable(
if e.stderr:
print('STDERR:', e.stderr)
return False
finally:
# Restore the original spec file if we modified it
if original_spec_content is not None:
with open(spec_file, 'w') as f:
f.write(original_spec_content)
print(f'🔄 Restored original {spec_file}')
# =================================================
@@ -254,6 +280,10 @@ def main() -> int:
action='store_true',
help='Install PyInstaller using uv before building',
)
parser.add_argument(
'--target-arch',
help='Target architecture for macOS builds (x86_64, arm64, universal2)',
)
parser.add_argument(
'--no-build', action='store_true', help='Skip testing the built executable'
@@ -270,7 +300,9 @@ def main() -> int:
return 1
# Build the executable
if not args.no_build and not build_executable(args.spec, clean=not args.no_clean):
if not args.no_build and not build_executable(
args.spec, clean=not args.no_clean, target_arch=args.target_arch
):
return 1
# Test the executable

View File

@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
[project]
name = "openhands"
version = "0.1.0"
version = "1.0.0"
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
readme = "README.md"
license = { text = "MIT" }