mcli-framework 8.0.48__tar.gz → 8.0.49__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcli_framework-8.0.48/src/mcli_framework.egg-info → mcli_framework-8.0.49}/PKG-INFO +2 -1
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/pyproject.toml +2 -1
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/main.py +9 -0
- mcli_framework-8.0.49/src/mcli/workflow/ci/__init__.py +1 -0
- mcli_framework-8.0.49/src/mcli/workflow/ci/act_runner.py +61 -0
- mcli_framework-8.0.49/src/mcli/workflow/ci/ci.py +172 -0
- mcli_framework-8.0.49/src/mcli/workflow/ci/runner_status.py +26 -0
- mcli_framework-8.0.49/src/mcli/workflow/ci/workflow_transform.py +151 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49/src/mcli_framework.egg-info}/PKG-INFO +2 -1
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/SOURCES.txt +5 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/requires.txt +1 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/LICENSE +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/MANIFEST.in +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/README.md +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/llms-full.txt +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/llms.txt +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/mcli_rust/Cargo.toml +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/mcli_rust/src/command_parser.rs +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/mcli_rust/src/file_watcher.rs +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/mcli_rust/src/lib.rs +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/mcli_rust/src/process_manager.rs +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/mcli_rust/src/tfidf.rs +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/setup.cfg +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/commands_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/completion_helpers.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/context_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/create_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/delete_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/edit_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/import_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/init_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/list_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/migrate_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/model/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/model/model.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/model_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/mv_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/new_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/remove_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/rm_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/search_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/services_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/setup_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/source_sync_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/sync_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/video/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/app/video/video.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/config.toml +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/api/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/api/api.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/api/daemon_client.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/api/daemon_client_local.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/api/daemon_decorator.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/api/mcli_decorators.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/auth.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/aws_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/azure_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/credential_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/gcp_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/key_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/mcli_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/token_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/auth/token_util.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/config/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/config/config.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/config/settings.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/commands.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/defaults.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/env.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/messages.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/paths.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/scripts.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/constants/storage.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/custom_commands.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/discovery/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/discovery/command_discovery.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/erd/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/erd/erd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/erd/generate_graph.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/errors.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/feature_detection.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/files/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/files/files.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/folder_workflows.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/fs/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/fs/fs.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/ipfs_sync.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/ipfs_utils.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/ipns_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/lib.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/logger/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/logger/correlation.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/logger/logger.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/logger/structured.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/optional_deps.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/paths.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/performance/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/performance/optimizer.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/performance/rust_bridge.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/performance/uvloop_config.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/pickles/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/pickles/pickles.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/deps.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/pyenv/venv.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/script_loader.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/script_sync.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/script_watcher.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/search/cached_vectorizer.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/secrets/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/secrets/commands.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/secrets/manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/secrets/repl.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/secrets/store.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/config.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/data_pipeline.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/health.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/lsh_client.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/redis_service.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/registry.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/state.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/services/supervisor.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/shell/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/shell/exceptions.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/shell/shell.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/sync_key_store.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/templates/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/templates/command_templates.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/toml/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/toml/toml.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/types.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/ui/styling.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/ui/visual_effects.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/watcher/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/watcher/watcher.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/workflow_models.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/lib/workspace_registry.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/mygroup/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/mygroup/test_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/public/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/public/commands/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/public/oi/oi.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/public/public.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/completion_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/env_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/health_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/ipfs_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/logs_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/migrate_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/release_notes_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/self_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/store_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/test_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/self/workflows_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/backends/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/backends/ipfs_backend.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/base.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/cache.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/encryption.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/factory.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/registry.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/storage/storacha_cli.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/async_command_database.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/async_process_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/client.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/daemon.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/daemon_api.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/enhanced_daemon.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/process_cli.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/process_manager.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/daemon/test_daemon.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/doc_convert.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/docker/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/docker/docker.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/file/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/gcloud/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/gcloud/config.toml +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/gcloud/gcloud.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/git_commit/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/git_commit/ai_service.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/interview/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/lsh_integration.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/client.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/download_and_run_efficient_models.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/lightweight_embedder.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/lightweight_model_server.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/lightweight_test.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/model_service.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/ollama_efficient_runner.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/openai_adapter.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/pdf_processor.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_efficient_runner.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_example.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_integration.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/model_service/test_new_features.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/command_loader.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/converter.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/executor.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/notebook_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/schema.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/notebook/validator.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/openai/openai.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/registry/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/registry/registry.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/repo/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/repo/repo.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/cron_parser.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/job.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/models.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/monitor.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/persistence.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/scheduler.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/scheduler/validation.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/search/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/secrets/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/secrets/secrets_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/storage/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/storage/storage_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/sync/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/sync/test_cmd.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/videos/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/wakatime/__init__.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/wakatime/wakatime.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli/workflow/workflow.py +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/dependency_links.txt +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/entry_points.txt +0 -0
- {mcli_framework-8.0.48 → mcli_framework-8.0.49}/src/mcli_framework.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcli-framework
|
|
3
|
-
Version: 8.0.
|
|
3
|
+
Version: 8.0.49
|
|
4
4
|
Summary: Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/workflows/, version with lockfile, run as daemon or cron job.
|
|
5
5
|
Author-email: Luis Fernandez de la Vara <luis@lefv.io>
|
|
6
6
|
Maintainer-email: Luis Fernandez de la Vara <luis@lefv.io>
|
|
@@ -37,6 +37,7 @@ Requires-Python: >=3.10
|
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
License-File: LICENSE
|
|
39
39
|
Requires-Dist: click<9.0.0,>=8.1.7
|
|
40
|
+
Requires-Dist: ruamel.yaml<0.19,>=0.18
|
|
40
41
|
Requires-Dist: rich<15.0.0,>=14.0.0
|
|
41
42
|
Requires-Dist: requests<3.0.0,>=2.31.0
|
|
42
43
|
Requires-Dist: tomli<3.0.0,>=2.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mcli-framework"
|
|
3
|
-
version = "8.0.
|
|
3
|
+
version = "8.0.49"
|
|
4
4
|
description = "Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/workflows/, version with lockfile, run as daemon or cron job."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -37,6 +37,7 @@ classifiers = [
|
|
|
37
37
|
dependencies = [
|
|
38
38
|
# Core CLI dependencies
|
|
39
39
|
"click>=8.1.7,<9.0.0",
|
|
40
|
+
"ruamel.yaml>=0.18,<0.19",
|
|
40
41
|
"rich>=14.0.0,<15.0.0",
|
|
41
42
|
"requests>=2.31.0,<3.0.0",
|
|
42
43
|
"tomli>=2.2.1,<3.0.0",
|
|
@@ -447,6 +447,15 @@ def _add_lazy_commands(app: click.Group):
|
|
|
447
447
|
except ImportError as e:
|
|
448
448
|
logger.debug(f"Could not load sync group: {e}")
|
|
449
449
|
|
|
450
|
+
# mcli ci - act-first CI gate + hosted-trigger migration
|
|
451
|
+
try:
|
|
452
|
+
from mcli.workflow.ci.ci import ci
|
|
453
|
+
|
|
454
|
+
app.add_command(ci, name="ci")
|
|
455
|
+
logger.debug("Added ci group")
|
|
456
|
+
except ImportError as e:
|
|
457
|
+
logger.debug(f"Could not load ci group: {e}")
|
|
458
|
+
|
|
450
459
|
# mcli setup - Onboarding wizard for new users
|
|
451
460
|
try:
|
|
452
461
|
from mcli.app.setup_cmd import setup
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""act-first CI tooling: local act gate + hosted-trigger stripping for private repos."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Run `act` locally and classify the outcome for the PR gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PreflightResult(Enum):
|
|
12
|
+
PASS = "pass"
|
|
13
|
+
FAIL = "fail"
|
|
14
|
+
UNREACHABLE = "unreachable"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def act_available() -> bool:
|
|
18
|
+
return shutil.which("act") is not None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def docker_running() -> bool:
|
|
22
|
+
try:
|
|
23
|
+
proc = subprocess.run(["docker", "info"], capture_output=True, timeout=30)
|
|
24
|
+
return proc.returncode == 0
|
|
25
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def probe() -> bool:
|
|
30
|
+
"""Can act actually run here? Needs the binary, a live docker daemon, and `act -l`."""
|
|
31
|
+
if not act_available() or not docker_running():
|
|
32
|
+
return False
|
|
33
|
+
try:
|
|
34
|
+
proc = subprocess.run(["act", "-l"], capture_output=True, text=True, timeout=60)
|
|
35
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
36
|
+
return False
|
|
37
|
+
return proc.returncode == 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_act_command(event: str) -> list[str]:
|
|
41
|
+
cmd = ["act", event]
|
|
42
|
+
if Path(".secrets").exists():
|
|
43
|
+
cmd += ["--secret-file", ".secrets"]
|
|
44
|
+
return cmd
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_act(event: str = "pull_request") -> PreflightResult:
|
|
48
|
+
"""Run act for `event`. PASS on exit 0, else FAIL. (Probe gates UNREACHABLE upstream.)"""
|
|
49
|
+
proc = subprocess.run(build_act_command(event))
|
|
50
|
+
return PreflightResult.PASS if proc.returncode == 0 else PreflightResult.FAIL
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def preflight(repo_slug: str, event: str = "pull_request") -> PreflightResult:
|
|
54
|
+
"""Primary gate. PASS/FAIL if act can run; UNREACHABLE if act can't start here.
|
|
55
|
+
|
|
56
|
+
`repo_slug` is accepted for symmetry and future use; the runner fallback is
|
|
57
|
+
orchestrated by the CLI layer based on runner_status.has_online_runner.
|
|
58
|
+
"""
|
|
59
|
+
if not probe():
|
|
60
|
+
return PreflightResult.UNREACHABLE
|
|
61
|
+
return run_act(event)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""`mcli ci` — act-first CI gate and hosted-trigger migration for private repos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import stat
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from mcli.workflow.ci.act_runner import PreflightResult, act_available, docker_running
|
|
13
|
+
from mcli.workflow.ci.act_runner import preflight as preflight_fn
|
|
14
|
+
from mcli.workflow.ci.runner_status import has_online_runner
|
|
15
|
+
from mcli.workflow.ci.workflow_transform import transform_file, write_self_hosted_workflow
|
|
16
|
+
|
|
17
|
+
_GITHUB_REMOTE_RE = re.compile(
|
|
18
|
+
r"(?:git@github\.com:|https://github\.com/)([^/]+/[^/]+?)(?:\.git)?/?$"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def current_repo_slug() -> str | None:
|
|
23
|
+
"""owner/name from a github.com origin remote, or None (non-GitHub or no remote)."""
|
|
24
|
+
try:
|
|
25
|
+
url = subprocess.run(
|
|
26
|
+
["git", "remote", "get-url", "origin"],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
timeout=10,
|
|
30
|
+
).stdout.strip()
|
|
31
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
32
|
+
return None
|
|
33
|
+
if not url:
|
|
34
|
+
return None
|
|
35
|
+
match = _GITHUB_REMOTE_RE.match(url)
|
|
36
|
+
return match.group(1) if match else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def detect_test_command() -> str:
|
|
40
|
+
"""Best-effort test command for the self-hosted fallback workflow."""
|
|
41
|
+
makefile = Path("Makefile")
|
|
42
|
+
if makefile.exists():
|
|
43
|
+
txt = makefile.read_text()
|
|
44
|
+
if "\ntest:" in txt or txt.startswith("test:"):
|
|
45
|
+
return "make test"
|
|
46
|
+
if Path("pyproject.toml").exists() or Path("pytest.ini").exists():
|
|
47
|
+
return "uv run pytest -v || pytest -v"
|
|
48
|
+
if Path("package.json").exists():
|
|
49
|
+
return "npm test"
|
|
50
|
+
if Path("mix.exs").exists():
|
|
51
|
+
return "mix test"
|
|
52
|
+
return "echo 'TODO: set test command' && exit 1"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def workflows_dir() -> Path:
|
|
56
|
+
return Path(".github") / "workflows"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@click.group()
|
|
60
|
+
def ci():
|
|
61
|
+
"""act-first CI: local act gate + stop billed hosted runners on private repos."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@ci.command()
|
|
65
|
+
@click.option("--dry-run", is_flag=True, help="Show what would change without writing.")
|
|
66
|
+
def migrate(dry_run):
|
|
67
|
+
"""Strip hosted triggers from this repo's workflows + add self-hosted fallback."""
|
|
68
|
+
wfdir = workflows_dir()
|
|
69
|
+
if not wfdir.exists():
|
|
70
|
+
click.echo("No .github/workflows directory; nothing to migrate.")
|
|
71
|
+
return
|
|
72
|
+
slug = current_repo_slug()
|
|
73
|
+
has_runner = has_online_runner(slug) if slug else False
|
|
74
|
+
test_cmd = detect_test_command()
|
|
75
|
+
|
|
76
|
+
files = sorted(p for p in wfdir.glob("*.y*ml") if p.name != "self-hosted-ci.yml")
|
|
77
|
+
if dry_run:
|
|
78
|
+
from mcli.workflow.ci.workflow_transform import MARKER, _yaml, workflow_has_hosted_job
|
|
79
|
+
|
|
80
|
+
for f in files:
|
|
81
|
+
text = f.read_text()
|
|
82
|
+
if MARKER in text:
|
|
83
|
+
click.echo(f" skip (already migrated): {f.name}")
|
|
84
|
+
continue
|
|
85
|
+
hosted = workflow_has_hosted_job(_yaml().load(text))
|
|
86
|
+
click.echo(f" {'STRIP' if hosted else 'keep '}: {f.name}")
|
|
87
|
+
click.echo(f" fallback self-hosted-ci.yml (pull_request={has_runner}, test='{test_cmd}')")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
changed = [f.name for f in files if transform_file(f)]
|
|
91
|
+
created = write_self_hosted_workflow(wfdir, test_cmd, with_pull_request=has_runner)
|
|
92
|
+
for name in changed:
|
|
93
|
+
click.echo(f" stripped: {name}")
|
|
94
|
+
if created:
|
|
95
|
+
click.echo(f" created: self-hosted-ci.yml (pull_request={has_runner})")
|
|
96
|
+
click.echo(f"Done. {len(changed)} workflow(s) migrated.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@ci.command()
|
|
100
|
+
@click.option("--event", default="pull_request", show_default=True, help="act event to simulate.")
|
|
101
|
+
def preflight(event):
|
|
102
|
+
"""Run act as the PR gate. Exit 0=pass, 1=fail, 2=cannot validate, 3=use runner."""
|
|
103
|
+
slug = current_repo_slug()
|
|
104
|
+
result = preflight_fn(slug, event)
|
|
105
|
+
if result == PreflightResult.PASS:
|
|
106
|
+
click.echo("✅ act passed — OK to open PR.")
|
|
107
|
+
raise SystemExit(0)
|
|
108
|
+
if result == PreflightResult.FAIL:
|
|
109
|
+
click.echo("❌ act failed — fix before opening PR.")
|
|
110
|
+
raise SystemExit(1)
|
|
111
|
+
# UNREACHABLE
|
|
112
|
+
if slug and has_online_runner(slug):
|
|
113
|
+
click.echo(
|
|
114
|
+
"⚠️ act unreachable here; an online runner exists — "
|
|
115
|
+
"push and let the self-hosted runner validate."
|
|
116
|
+
)
|
|
117
|
+
raise SystemExit(3)
|
|
118
|
+
click.echo("⚠️ act unreachable and no online runner — cannot validate this PR.")
|
|
119
|
+
raise SystemExit(2)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@ci.command()
|
|
123
|
+
def pr():
|
|
124
|
+
"""preflight, then `gh pr create --fill --base main` if it passed."""
|
|
125
|
+
slug = current_repo_slug()
|
|
126
|
+
result = preflight_fn(slug)
|
|
127
|
+
if result == PreflightResult.PASS:
|
|
128
|
+
# check=False on purpose: let gh/git stream their own errors to the terminal
|
|
129
|
+
# rather than raising CalledProcessError and hiding their output.
|
|
130
|
+
subprocess.run(["gh", "pr", "create", "--fill", "--base", "main"], check=False)
|
|
131
|
+
return
|
|
132
|
+
if result == PreflightResult.FAIL:
|
|
133
|
+
click.echo("act failed; not opening PR.")
|
|
134
|
+
raise SystemExit(1)
|
|
135
|
+
if slug and has_online_runner(slug):
|
|
136
|
+
click.echo("act unreachable; pushing so the runner can validate.")
|
|
137
|
+
subprocess.run(["git", "push", "-u", "origin", "HEAD"], check=False)
|
|
138
|
+
subprocess.run(["gh", "pr", "create", "--fill", "--base", "main"], check=False)
|
|
139
|
+
return
|
|
140
|
+
click.echo("act unreachable and no runner; refusing to open an unvalidated PR.")
|
|
141
|
+
raise SystemExit(2)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
PRE_PUSH_HOOK = """#!/usr/bin/env bash
|
|
145
|
+
# mcli-ci pre-push gate: validate with act before pushing.
|
|
146
|
+
exec mcli ci preflight
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@ci.command()
|
|
151
|
+
def doctor():
|
|
152
|
+
"""Show act/docker/runner status for this repo."""
|
|
153
|
+
click.echo(f"act installed: {act_available()}")
|
|
154
|
+
click.echo(f"docker running: {docker_running()}")
|
|
155
|
+
slug = current_repo_slug()
|
|
156
|
+
click.echo(f"repo: {slug or '(no origin)'}")
|
|
157
|
+
if slug:
|
|
158
|
+
click.echo(f"online runner: {has_online_runner(slug)}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@ci.command(name="install-hook")
|
|
162
|
+
def install_hook():
|
|
163
|
+
"""Install an opt-in pre-push hook that runs `mcli ci preflight`."""
|
|
164
|
+
hooks = Path(".git") / "hooks"
|
|
165
|
+
if not hooks.exists():
|
|
166
|
+
click.echo("Not a git repo (.git/hooks missing).")
|
|
167
|
+
raise SystemExit(1)
|
|
168
|
+
hook = hooks / "pre-push"
|
|
169
|
+
hook.write_text(PRE_PUSH_HOOK)
|
|
170
|
+
mode = hook.stat().st_mode
|
|
171
|
+
hook.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
172
|
+
click.echo(f"Installed pre-push hook at {hook}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Query GitHub for self-hosted runner availability via the gh CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def has_online_runner(repo_slug: str) -> bool:
|
|
10
|
+
"""True if `repo_slug` (owner/name) has at least one online self-hosted runner."""
|
|
11
|
+
try:
|
|
12
|
+
proc = subprocess.run(
|
|
13
|
+
["gh", "api", f"repos/{repo_slug}/actions/runners"],
|
|
14
|
+
capture_output=True,
|
|
15
|
+
text=True,
|
|
16
|
+
timeout=30,
|
|
17
|
+
)
|
|
18
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
19
|
+
return False
|
|
20
|
+
if proc.returncode != 0:
|
|
21
|
+
return False
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(proc.stdout or "{}")
|
|
24
|
+
except json.JSONDecodeError:
|
|
25
|
+
return False
|
|
26
|
+
return any(r.get("status") == "online" for r in data.get("runners", []))
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Transform GitHub Actions workflows for act-first CI on private repos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ruamel.yaml import YAML
|
|
9
|
+
|
|
10
|
+
MARKER = "mcli-ci: hosted-triggers-stripped"
|
|
11
|
+
SELF_HOSTED_FILENAME = "self-hosted-ci.yml"
|
|
12
|
+
HOSTED_PREFIXES = ("ubuntu", "macos", "windows")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_hosted_label(label: str) -> bool:
|
|
16
|
+
"""True if a runs-on label names a GitHub-hosted runner image."""
|
|
17
|
+
return str(label).lower().startswith(HOSTED_PREFIXES)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def runs_on_is_hosted(runs_on) -> bool:
|
|
21
|
+
"""Classify a job's runs-on. Conservative: unknown expressions count as hosted."""
|
|
22
|
+
if runs_on is None:
|
|
23
|
+
return False
|
|
24
|
+
if isinstance(runs_on, (list, tuple)):
|
|
25
|
+
labels = [str(x) for x in runs_on]
|
|
26
|
+
if any("self-hosted" in lbl for lbl in labels):
|
|
27
|
+
return False
|
|
28
|
+
return any(is_hosted_label(lbl) for lbl in labels)
|
|
29
|
+
text = str(runs_on)
|
|
30
|
+
if "self-hosted" in text:
|
|
31
|
+
return False
|
|
32
|
+
if "${{" in text:
|
|
33
|
+
return True # unknown matrix/expression -> assume hosted to stop cost
|
|
34
|
+
return is_hosted_label(text)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# NOTE: ruamel.yaml round-trip (YAML(), typ='rt') is the SAFE, correct loader here.
|
|
38
|
+
# It does NOT execute `!!python/object` tags like PyYAML's yaml.load(). Do not
|
|
39
|
+
# "fix" this to PyYAML safe_load — that would strip comments/formatting and break
|
|
40
|
+
# round-tripping. Round-trip preservation is a hard requirement of this transform.
|
|
41
|
+
def _yaml() -> YAML:
|
|
42
|
+
y = YAML()
|
|
43
|
+
y.version = (1, 2) # critical: keeps bare `on:` a string, not boolean True
|
|
44
|
+
y.preserve_quotes = True
|
|
45
|
+
y.width = 4096 # avoid reflowing long lines
|
|
46
|
+
y.indent(mapping=2, sequence=4, offset=2)
|
|
47
|
+
return y
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def workflow_has_hosted_job(doc) -> bool:
|
|
51
|
+
"""True if any job in the parsed workflow targets a GitHub-hosted runner."""
|
|
52
|
+
if not isinstance(doc, dict):
|
|
53
|
+
return False
|
|
54
|
+
jobs = doc.get("jobs") or {}
|
|
55
|
+
for job in jobs.values():
|
|
56
|
+
if isinstance(job, dict) and runs_on_is_hosted(job.get("runs-on")):
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def strip_hosted_triggers(doc) -> bool:
|
|
62
|
+
"""Remove push/pull_request from `on:`, ensure workflow_dispatch. Returns changed."""
|
|
63
|
+
on = doc.get("on")
|
|
64
|
+
if on is None:
|
|
65
|
+
return False
|
|
66
|
+
changed = False
|
|
67
|
+
if isinstance(on, dict):
|
|
68
|
+
for key in ("push", "pull_request"):
|
|
69
|
+
if key in on:
|
|
70
|
+
del on[key]
|
|
71
|
+
changed = True
|
|
72
|
+
if "workflow_dispatch" not in on:
|
|
73
|
+
on["workflow_dispatch"] = None
|
|
74
|
+
changed = True
|
|
75
|
+
elif isinstance(on, list):
|
|
76
|
+
for key in ("push", "pull_request"):
|
|
77
|
+
while key in on:
|
|
78
|
+
on.remove(key)
|
|
79
|
+
changed = True
|
|
80
|
+
if "workflow_dispatch" not in on:
|
|
81
|
+
on.append("workflow_dispatch")
|
|
82
|
+
changed = True
|
|
83
|
+
elif isinstance(on, str):
|
|
84
|
+
if on in ("push", "pull_request"):
|
|
85
|
+
doc["on"] = "workflow_dispatch"
|
|
86
|
+
changed = True
|
|
87
|
+
return changed
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def transform_file(path: Path) -> bool:
|
|
91
|
+
"""Strip hosted triggers in-place if the workflow has a hosted job. Idempotent."""
|
|
92
|
+
path = Path(path)
|
|
93
|
+
text = path.read_text()
|
|
94
|
+
if MARKER in text:
|
|
95
|
+
return False
|
|
96
|
+
yaml = _yaml()
|
|
97
|
+
doc = yaml.load(text)
|
|
98
|
+
if not workflow_has_hosted_job(doc):
|
|
99
|
+
return False
|
|
100
|
+
# `on:` is workflow-level. If a workflow mixes hosted and self-hosted jobs,
|
|
101
|
+
# stripping push/pull_request also removes the self-hosted job's auto-trigger.
|
|
102
|
+
# Acceptable here: the separate self-hosted-ci.yml provides the runner PR path
|
|
103
|
+
# when a runner exists. Re-add triggers by hand if a single workflow needs both.
|
|
104
|
+
strip_hosted_triggers(doc)
|
|
105
|
+
doc.yaml_set_start_comment(f"{MARKER}\n")
|
|
106
|
+
# Pinning version=(1,2) at load keeps `on` a string key (not YAML 1.1 bool True).
|
|
107
|
+
# Reset it before dumping so ruamel does not prepend a `%YAML 1.2` / `---` directive
|
|
108
|
+
# to the workflow file (the key is already a plain string in the loaded document).
|
|
109
|
+
yaml.version = None
|
|
110
|
+
buf = StringIO()
|
|
111
|
+
yaml.dump(doc, buf)
|
|
112
|
+
path.write_text(buf.getvalue())
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def render_self_hosted_workflow(test_command: str, with_pull_request: bool) -> str:
|
|
117
|
+
"""Render the dormant self-hosted fallback workflow as YAML text."""
|
|
118
|
+
triggers = " workflow_dispatch:\n"
|
|
119
|
+
if with_pull_request:
|
|
120
|
+
triggers += " pull_request:\n"
|
|
121
|
+
ref = "${{ github.ref }}"
|
|
122
|
+
return (
|
|
123
|
+
f"# {MARKER}\n"
|
|
124
|
+
"name: self-hosted-ci\n"
|
|
125
|
+
"on:\n"
|
|
126
|
+
f"{triggers}"
|
|
127
|
+
"concurrency:\n"
|
|
128
|
+
f" group: self-hosted-ci-{ref}\n"
|
|
129
|
+
" cancel-in-progress: true\n"
|
|
130
|
+
"jobs:\n"
|
|
131
|
+
" test:\n"
|
|
132
|
+
" runs-on: [self-hosted, Linux, X64]\n"
|
|
133
|
+
" timeout-minutes: 30\n"
|
|
134
|
+
" steps:\n"
|
|
135
|
+
" - uses: actions/checkout@v4\n"
|
|
136
|
+
" - name: Run tests\n"
|
|
137
|
+
f" run: {test_command}\n"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def write_self_hosted_workflow(
|
|
142
|
+
workflows_dir: Path, test_command: str, with_pull_request: bool
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Write self-hosted-ci.yml if absent. Returns True if created."""
|
|
145
|
+
workflows_dir = Path(workflows_dir)
|
|
146
|
+
target = workflows_dir / SELF_HOSTED_FILENAME
|
|
147
|
+
if target.exists():
|
|
148
|
+
return False
|
|
149
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
target.write_text(render_self_hosted_workflow(test_command, with_pull_request))
|
|
151
|
+
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcli-framework
|
|
3
|
-
Version: 8.0.
|
|
3
|
+
Version: 8.0.49
|
|
4
4
|
Summary: Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/workflows/, version with lockfile, run as daemon or cron job.
|
|
5
5
|
Author-email: Luis Fernandez de la Vara <luis@lefv.io>
|
|
6
6
|
Maintainer-email: Luis Fernandez de la Vara <luis@lefv.io>
|
|
@@ -37,6 +37,7 @@ Requires-Python: >=3.10
|
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
License-File: LICENSE
|
|
39
39
|
Requires-Dist: click<9.0.0,>=8.1.7
|
|
40
|
+
Requires-Dist: ruamel.yaml<0.19,>=0.18
|
|
40
41
|
Requires-Dist: rich<15.0.0,>=14.0.0
|
|
41
42
|
Requires-Dist: requests<3.0.0,>=2.31.0
|
|
42
43
|
Requires-Dist: tomli<3.0.0,>=2.2.1
|
|
@@ -165,6 +165,11 @@ src/mcli/workflow/__init__.py
|
|
|
165
165
|
src/mcli/workflow/doc_convert.py
|
|
166
166
|
src/mcli/workflow/lsh_integration.py
|
|
167
167
|
src/mcli/workflow/workflow.py
|
|
168
|
+
src/mcli/workflow/ci/__init__.py
|
|
169
|
+
src/mcli/workflow/ci/act_runner.py
|
|
170
|
+
src/mcli/workflow/ci/ci.py
|
|
171
|
+
src/mcli/workflow/ci/runner_status.py
|
|
172
|
+
src/mcli/workflow/ci/workflow_transform.py
|
|
168
173
|
src/mcli/workflow/daemon/__init__.py
|
|
169
174
|
src/mcli/workflow/daemon/async_command_database.py
|
|
170
175
|
src/mcli/workflow/daemon/async_process_manager.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|