mcli-framework 8.0.62__tar.gz → 8.0.65__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.62/src/mcli_framework.egg-info → mcli_framework-8.0.65}/PKG-INFO +1 -1
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/pyproject.toml +1 -1
- mcli_framework-8.0.65/src/mcli/workflow/ci/act_lock.py +171 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/act_runner.py +114 -54
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/ci.py +32 -3
- {mcli_framework-8.0.62 → mcli_framework-8.0.65/src/mcli_framework.egg-info}/PKG-INFO +1 -1
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/SOURCES.txt +1 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/LICENSE +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/MANIFEST.in +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/README.md +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/llms-full.txt +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/llms.txt +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/Cargo.toml +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/command_parser.rs +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/file_watcher.rs +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/lib.rs +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/process_manager.rs +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/tfidf.rs +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/setup.cfg +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/__main__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/completion_helpers.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/context_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/delete_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/edit_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/import_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/init_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/list_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/main.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/model/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/model/model.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/model_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/mv_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/new_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/search_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/services_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/setup_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/source_sync_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/sync_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/video/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/video/video.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/config.toml +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/api.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/daemon_client.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/daemon_client_local.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/daemon_decorator.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/mcli_decorators.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/auth.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/aws_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/azure_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/credential_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/gcp_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/key_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/mcli_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/token_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/token_util.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/config/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/config/config.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/config/settings.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/commands.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/defaults.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/env.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/messages.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/paths.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/scripts.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/storage.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/custom_commands.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/discovery/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/discovery/command_discovery.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/erd/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/erd/erd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/erd/generate_graph.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/errors.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/feature_detection.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/files/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/files/files.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/folder_workflows.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/fs/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/fs/fs.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ipfs_sync.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ipfs_utils.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ipns_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/lib.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/correlation.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/logger.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/structured.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/optional_deps.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/paths.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/optimizer.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/rust_bridge.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/uvloop_config.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pickles/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pickles/pickles.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/deps.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/venv.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/script_loader.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/script_sync.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/script_watcher.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/search/cached_vectorizer.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/commands.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/repl.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/store.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/config.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/data_pipeline.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/health.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/lsh_client.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/redis_service.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/registry.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/state.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/supervisor.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/shell/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/shell/exceptions.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/shell/shell.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/sync_key_store.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/templates/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/templates/command_templates.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/toml/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/toml/toml.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/types.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ui/styling.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ui/visual_effects.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/watcher/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/watcher/watcher.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/workflow_models.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/workflow_runtime.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/workspace_registry.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/mygroup/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/mygroup/test_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/commands/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/oi/oi.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/public.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/completion_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/env_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/health_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/ipfs_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/logs_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/migrate_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/release_notes_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/self_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/store_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/test_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/workflows_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/backends/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/backends/ipfs_backend.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/base.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/cache.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/encryption.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/factory.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/registry.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/storacha_cli.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/runner_status.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/workflow_transform.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/async_command_database.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/async_process_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/client.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/daemon.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/daemon_api.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/enhanced_daemon.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/process_cli.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/process_manager.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/test_daemon.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/doc_convert.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/docker/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/docker/docker.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/file/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/gcloud/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/gcloud/config.toml +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/gcloud/gcloud.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/git_commit/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/git_commit/ai_service.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/interview/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/lsh_integration.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/client.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/download_and_run_efficient_models.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/lightweight_embedder.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/lightweight_model_server.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/lightweight_test.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/model_service.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/ollama_efficient_runner.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/openai_adapter.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/pdf_processor.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_efficient_runner.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_example.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_integration.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_new_features.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/command_loader.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/converter.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/executor.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/notebook_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/schema.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/validator.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/openai/openai.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/registry/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/registry/registry.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/repo/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/repo/repo.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/cron_parser.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/job.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/models.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/monitor.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/persistence.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/scheduler.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/validation.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/search/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/secrets/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/secrets/secrets_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/storage/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/storage/storage_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/sync/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/sync/test_cmd.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/videos/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/wakatime/__init__.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/wakatime/wakatime.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/workflow.py +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/dependency_links.txt +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/entry_points.txt +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/requires.txt +0 -0
- {mcli_framework-8.0.62 → mcli_framework-8.0.65}/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.65
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mcli-framework"
|
|
3
|
-
version = "8.0.
|
|
3
|
+
version = "8.0.65"
|
|
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"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Machine-wide advisory lock that serialises `mcli ci preflight` act runs.
|
|
2
|
+
|
|
3
|
+
Every repo that ran `mcli ci install-hook` fires `mcli ci preflight` from its
|
|
4
|
+
pre-push hook. With several agents (or several terminals) pushing at once, that
|
|
5
|
+
starts several `act` runs at once, and each one drives the same container
|
|
6
|
+
runtime — one podman/docker VM with a fixed CPU/memory budget shared with
|
|
7
|
+
everything else on the host. Concurrent runs starve it; the observed symptom was
|
|
8
|
+
an Android emulator ANR'ing under test while act runs churned.
|
|
9
|
+
|
|
10
|
+
This module gives the act path a single machine-wide gate:
|
|
11
|
+
|
|
12
|
+
* Exclusion is `fcntl.flock(LOCK_EX)` on a lockfile in the mcli home. flock is
|
|
13
|
+
owned by the open file description, so the kernel drops it when the holder
|
|
14
|
+
exits — normally, by exception, or by SIGKILL. There is nothing to clean up
|
|
15
|
+
and nothing to go stale.
|
|
16
|
+
* The PID/repo written into the lockfile is *informational only*: it makes the
|
|
17
|
+
"waiting for…" message useful. Correctness never reads it.
|
|
18
|
+
* Waiting is bounded. On expiry the caller is told it did not get the lock and
|
|
19
|
+
decides what to do — a gate that can block a push forever is worse than one
|
|
20
|
+
that occasionally cannot validate.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import errno
|
|
26
|
+
import fcntl
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
from collections.abc import Iterator
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Callable
|
|
35
|
+
|
|
36
|
+
from mcli.lib.paths import get_mcli_home
|
|
37
|
+
|
|
38
|
+
# Ten minutes: long enough for a slow act run ahead of us to finish, short
|
|
39
|
+
# enough that a wedged machine never holds a push hostage for a workday.
|
|
40
|
+
DEFAULT_LOCK_TIMEOUT_SECONDS = 600.0
|
|
41
|
+
|
|
42
|
+
LOCK_TIMEOUT_ENV = "MCLI_CI_LOCK_TIMEOUT"
|
|
43
|
+
LOCK_PATH_ENV = "MCLI_CI_LOCK_PATH"
|
|
44
|
+
|
|
45
|
+
_LOCK_DIR_NAME = "locks"
|
|
46
|
+
_LOCK_FILE_NAME = "ci-act.lock"
|
|
47
|
+
|
|
48
|
+
# Retry cadence while waiting. flock's blocking mode cannot be bounded
|
|
49
|
+
# portably, so poll the non-blocking variant instead.
|
|
50
|
+
_POLL_INTERVAL_SECONDS = 0.5
|
|
51
|
+
|
|
52
|
+
_UNKNOWN_HOLDER = "holder unknown"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def lock_path() -> Path:
|
|
56
|
+
"""The one lockfile every repo on this machine contends for.
|
|
57
|
+
|
|
58
|
+
Deliberately NOT per-repo: the resource being protected is the host's
|
|
59
|
+
container runtime, which every repo shares.
|
|
60
|
+
"""
|
|
61
|
+
override = os.getenv(LOCK_PATH_ENV)
|
|
62
|
+
if override:
|
|
63
|
+
return Path(override)
|
|
64
|
+
return get_mcli_home() / _LOCK_DIR_NAME / _LOCK_FILE_NAME
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_timeout(timeout: float | None = None) -> float:
|
|
68
|
+
"""Explicit argument beats `MCLI_CI_LOCK_TIMEOUT` beats the 600s default.
|
|
69
|
+
|
|
70
|
+
An unparsable env value falls back to the default rather than raising: a
|
|
71
|
+
typo in a shell profile must not break every push on the machine.
|
|
72
|
+
"""
|
|
73
|
+
if timeout is not None:
|
|
74
|
+
return float(timeout)
|
|
75
|
+
raw = os.getenv(LOCK_TIMEOUT_ENV)
|
|
76
|
+
if raw:
|
|
77
|
+
try:
|
|
78
|
+
return float(raw)
|
|
79
|
+
except ValueError:
|
|
80
|
+
pass
|
|
81
|
+
return DEFAULT_LOCK_TIMEOUT_SECONDS
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _write_holder(handle) -> None:
|
|
85
|
+
"""Record who holds the lock. Informational only — never load-bearing."""
|
|
86
|
+
try:
|
|
87
|
+
os.ftruncate(handle.fileno(), 0)
|
|
88
|
+
handle.seek(0)
|
|
89
|
+
stamp = datetime.now().isoformat(timespec="seconds")
|
|
90
|
+
handle.write(f"pid={os.getpid()} repo={os.getcwd()} since={stamp}\n")
|
|
91
|
+
handle.flush()
|
|
92
|
+
except OSError:
|
|
93
|
+
pass # metadata is a nicety; never fail a run over it
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _holder_description(path: Path) -> str:
|
|
97
|
+
"""Best-effort description of the current holder, for the wait message."""
|
|
98
|
+
try:
|
|
99
|
+
text = path.read_text().strip()
|
|
100
|
+
except OSError:
|
|
101
|
+
return _UNKNOWN_HOLDER
|
|
102
|
+
return text.splitlines()[0] if text else _UNKNOWN_HOLDER
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@contextmanager
|
|
106
|
+
def act_lock(
|
|
107
|
+
timeout: float | None = None,
|
|
108
|
+
path: Path | None = None,
|
|
109
|
+
announce: Callable[[str], object] | None = None,
|
|
110
|
+
) -> Iterator[bool]:
|
|
111
|
+
"""Hold the machine-wide act lock for the duration of the block.
|
|
112
|
+
|
|
113
|
+
Yields True when the lock was acquired and False when the wait timed out —
|
|
114
|
+
it never raises on contention, so the caller stays in control of what a
|
|
115
|
+
missed lock means.
|
|
116
|
+
"""
|
|
117
|
+
wait_budget = resolve_timeout(timeout)
|
|
118
|
+
lockfile = path if path is not None else lock_path()
|
|
119
|
+
emit = announce if announce is not None else sys.stdout.write
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
lockfile.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
handle = open(lockfile, "a+")
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
# Read-only home, a lockfile owned by another user, no space… none of
|
|
126
|
+
# that is a reason to fail a push. Degrade to the pre-lock behaviour
|
|
127
|
+
# (unserialised) and say so, rather than raising into the gate — an
|
|
128
|
+
# exception here would exit 1 and BLOCK the push in every repo.
|
|
129
|
+
emit(
|
|
130
|
+
f"⚠️ Cannot use the machine-wide act lock at {lockfile} ({exc}); "
|
|
131
|
+
"running act unserialised.\n"
|
|
132
|
+
)
|
|
133
|
+
yield True
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
acquired = False
|
|
137
|
+
try:
|
|
138
|
+
deadline = time.monotonic() + wait_budget
|
|
139
|
+
announced = False
|
|
140
|
+
while True:
|
|
141
|
+
try:
|
|
142
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
143
|
+
acquired = True
|
|
144
|
+
break
|
|
145
|
+
except OSError as exc:
|
|
146
|
+
if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EWOULDBLOCK):
|
|
147
|
+
raise
|
|
148
|
+
remaining = deadline - time.monotonic()
|
|
149
|
+
if remaining <= 0:
|
|
150
|
+
break
|
|
151
|
+
if not announced:
|
|
152
|
+
emit(
|
|
153
|
+
f"⏳ Another `mcli ci preflight` is already running act "
|
|
154
|
+
f"({_holder_description(lockfile)}); waiting up to "
|
|
155
|
+
f"{wait_budget:.0f}s for the machine-wide CI lock…\n"
|
|
156
|
+
)
|
|
157
|
+
announced = True
|
|
158
|
+
time.sleep(min(_POLL_INTERVAL_SECONDS, remaining))
|
|
159
|
+
|
|
160
|
+
if acquired:
|
|
161
|
+
_write_holder(handle)
|
|
162
|
+
yield acquired
|
|
163
|
+
finally:
|
|
164
|
+
# Closing the descriptor releases the flock by itself; unlocking first
|
|
165
|
+
# is belt-and-braces in case the handle ever outlives this frame.
|
|
166
|
+
if acquired:
|
|
167
|
+
try:
|
|
168
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
169
|
+
except OSError:
|
|
170
|
+
pass
|
|
171
|
+
handle.close()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import platform
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
@@ -10,6 +11,8 @@ import time
|
|
|
10
11
|
from enum import Enum
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
14
|
+
from mcli.workflow.ci.act_lock import LOCK_TIMEOUT_ENV, act_lock, resolve_timeout
|
|
15
|
+
|
|
13
16
|
# Docker Hub returns these when the unauthenticated image-pull rate limit is hit.
|
|
14
17
|
# act surfaces them while pulling the runner image — this is an environment
|
|
15
18
|
# problem, NOT a test failure, so it must not hard-block a push.
|
|
@@ -28,16 +31,20 @@ _NO_STAGES_MARKERS = ("could not find any stages to run",)
|
|
|
28
31
|
_RETRY_BACKOFF = (15, 45)
|
|
29
32
|
_MAX_RETRIES = 2
|
|
30
33
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
_DISPATCH_WORKFLOW = Path(".github/workflows/ci.yml")
|
|
34
|
+
# Dispatch-only workflows that are safe and expected to run as local gates.
|
|
35
|
+
# Deploy, release, and audit workflows are intentionally not selected.
|
|
36
|
+
_WORKFLOW_DIR = Path(".github/workflows")
|
|
37
|
+
_DISPATCH_GATE_NAMES = {"ci", "secret-scan", "security-scan"}
|
|
36
38
|
|
|
37
39
|
# Header label of the job-id column in `act --list` table output.
|
|
38
40
|
_JOB_ID_HEADER = "Job ID"
|
|
39
41
|
|
|
40
42
|
|
|
43
|
+
def _act_command(*args: str) -> list[str]:
|
|
44
|
+
"""Build an act command without implicitly loading the repository .env."""
|
|
45
|
+
return ["act", *args, "--env-file", os.devnull]
|
|
46
|
+
|
|
47
|
+
|
|
41
48
|
class PreflightResult(Enum):
|
|
42
49
|
PASS = "pass"
|
|
43
50
|
FAIL = "fail"
|
|
@@ -98,7 +105,7 @@ def probe() -> bool:
|
|
|
98
105
|
if not act_available() or not docker_running():
|
|
99
106
|
return False
|
|
100
107
|
try:
|
|
101
|
-
proc = subprocess.run(
|
|
108
|
+
proc = subprocess.run(_act_command("-l"), capture_output=True, text=True, timeout=60)
|
|
102
109
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
103
110
|
return False
|
|
104
111
|
return proc.returncode == 0
|
|
@@ -107,38 +114,56 @@ def probe() -> bool:
|
|
|
107
114
|
def default_container_arch() -> str | None:
|
|
108
115
|
"""`--container-architecture` value for the host, or None to let act decide.
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
needs nothing.
|
|
117
|
+
On Apple Silicon, select native arm64 together with the multi-arch runner
|
|
118
|
+
mapping added by build_act_command; native Linux needs no override.
|
|
113
119
|
"""
|
|
114
120
|
if sys.platform == "darwin" and platform.machine() in ("arm64", "aarch64"):
|
|
115
|
-
return "linux/
|
|
121
|
+
return "linux/arm64"
|
|
116
122
|
return None
|
|
117
123
|
|
|
118
124
|
|
|
119
|
-
def
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
def dispatch_workflows() -> list[str]:
|
|
126
|
+
"""Return dispatch-only CI and security workflows safe to run locally.
|
|
127
|
+
|
|
128
|
+
The act-first migration historically preserved workflow filenames, so a
|
|
129
|
+
real gate may be ``elixir-ci.yml`` rather than the newer canonical
|
|
130
|
+
``ci.yml``. Select those CI names plus explicit security scans, while
|
|
131
|
+
excluding the dormant self-hosted fallback and every deploy/release job.
|
|
132
|
+
"""
|
|
133
|
+
if not _WORKFLOW_DIR.is_dir():
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
workflows: list[str] = []
|
|
137
|
+
for path in sorted((*_WORKFLOW_DIR.glob("*.yml"), *_WORKFLOW_DIR.glob("*.yaml"))):
|
|
138
|
+
stem = path.stem.lower()
|
|
139
|
+
is_ci = stem == "ci" or stem.startswith("ci-") or stem.endswith("-ci")
|
|
140
|
+
if stem == "self-hosted-ci":
|
|
141
|
+
continue
|
|
142
|
+
if is_ci or stem in _DISPATCH_GATE_NAMES:
|
|
143
|
+
workflows.append(str(path))
|
|
144
|
+
return workflows
|
|
124
145
|
|
|
125
146
|
|
|
126
|
-
def list_jobs(event: str, workflow: str | None = None) -> list[str]:
|
|
147
|
+
def list_jobs(event: str, workflow: str | None = None) -> list[str] | None:
|
|
127
148
|
"""Real job ids act would run for `event`, parsed from the `act --list` table.
|
|
128
149
|
|
|
129
150
|
`act --list` prints a table whose first row is a header containing a `Job ID`
|
|
130
151
|
column; each subsequent row is a runnable job. Returns job ids in table order
|
|
131
152
|
(deduplicated). Returns ``[]`` when act lists no jobs for the event (including
|
|
132
|
-
the "could not find any stages" no-op)
|
|
133
|
-
|
|
153
|
+
the "could not find any stages" no-op). Returns ``None`` when act cannot be
|
|
154
|
+
invoked or listing fails, so callers cannot mistake an error for a green
|
|
155
|
+
workflow with no jobs.
|
|
134
156
|
"""
|
|
135
|
-
cmd =
|
|
157
|
+
cmd = _act_command(event, "--list")
|
|
136
158
|
if workflow is not None:
|
|
137
159
|
cmd += ["-W", workflow]
|
|
138
160
|
try:
|
|
139
161
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
140
162
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
141
|
-
return
|
|
163
|
+
return None
|
|
164
|
+
output = (proc.stdout or "") + (proc.stderr or "")
|
|
165
|
+
if proc.returncode != 0:
|
|
166
|
+
return [] if _has_no_stages(output) else None
|
|
142
167
|
return _parse_job_ids(proc.stdout or "")
|
|
143
168
|
|
|
144
169
|
|
|
@@ -169,13 +194,19 @@ def build_act_command(
|
|
|
169
194
|
job: str | None = None,
|
|
170
195
|
arch: str | None = None,
|
|
171
196
|
) -> list[str]:
|
|
172
|
-
cmd =
|
|
197
|
+
cmd = _act_command(event)
|
|
173
198
|
if workflow is not None:
|
|
174
199
|
cmd += ["-W", workflow]
|
|
175
200
|
if job is not None:
|
|
176
201
|
cmd += ["-j", job]
|
|
177
202
|
if arch is not None:
|
|
178
203
|
cmd += ["--container-architecture", arch]
|
|
204
|
+
if arch == "linux/arm64":
|
|
205
|
+
# Use act's full multi-arch runner on Apple Silicon. Emulating BEAM
|
|
206
|
+
# and Dialyzer under QEMU is resource-heavy and prone to
|
|
207
|
+
# compiler/PLT segfaults.
|
|
208
|
+
cmd += ["--env", "RUNNER_ARCH=ARM64"]
|
|
209
|
+
cmd += ["-P", "ubuntu-latest=catthehacker/ubuntu:act-20.04"]
|
|
179
210
|
if Path(".secrets").exists():
|
|
180
211
|
cmd += ["--secret-file", ".secrets"]
|
|
181
212
|
return cmd
|
|
@@ -234,10 +265,9 @@ def run_act(
|
|
|
234
265
|
- any other non-zero exit -> FAIL.
|
|
235
266
|
|
|
236
267
|
Migrated repos are ``workflow_dispatch``-only, so the default
|
|
237
|
-
``pull_request`` event matches no jobs. Rather than hollow-pass,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
keeps the repo's original name) and run each so the gate validates for real.
|
|
268
|
+
``pull_request`` event matches no jobs. Rather than hollow-pass, discover
|
|
269
|
+
CI and security gate workflows and run their real job ids via
|
|
270
|
+
``workflow_dispatch``. Deploy and release workflows are never selected.
|
|
241
271
|
|
|
242
272
|
A green result means act actually executed ≥1 job and every job passed. If
|
|
243
273
|
``act --list`` shows jobs but a run reports "could not find any stages"
|
|
@@ -249,46 +279,58 @@ def run_act(
|
|
|
249
279
|
if not (result == PreflightResult.PASS and _has_no_stages(output)):
|
|
250
280
|
return result
|
|
251
281
|
|
|
252
|
-
# No stages for the requested event — try
|
|
253
|
-
|
|
254
|
-
if
|
|
282
|
+
# No stages for the requested event — try safe workflow_dispatch gates.
|
|
283
|
+
workflows = dispatch_workflows()
|
|
284
|
+
if not workflows:
|
|
255
285
|
sys.stdout.write(
|
|
256
|
-
"ℹ️ No act stages for this event and no
|
|
286
|
+
"ℹ️ No act stages for this event and no CI/security dispatch "
|
|
257
287
|
"entrypoint — nothing to validate; treating as pass.\n"
|
|
258
288
|
)
|
|
259
289
|
return PreflightResult.PASS
|
|
260
290
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
ran_job = False
|
|
292
|
+
for workflow in workflows:
|
|
293
|
+
jobs = list_jobs("workflow_dispatch", workflow)
|
|
294
|
+
if jobs is None:
|
|
295
|
+
sys.stdout.write(
|
|
296
|
+
f"❌ Failed to discover workflow_dispatch jobs for {workflow}; "
|
|
297
|
+
"the gate was not executed.\n"
|
|
298
|
+
)
|
|
299
|
+
return PreflightResult.FAIL
|
|
300
|
+
if not jobs:
|
|
301
|
+
sys.stdout.write(f"ℹ️ {workflow} has no workflow_dispatch jobs — skipping.\n")
|
|
302
|
+
continue
|
|
303
|
+
|
|
264
304
|
sys.stdout.write(
|
|
265
|
-
f"ℹ️ {
|
|
266
|
-
"
|
|
305
|
+
f"ℹ️ No '{event}' stages (workflow_dispatch-only); running "
|
|
306
|
+
f"{workflow} {jobs} via workflow_dispatch…\n"
|
|
267
307
|
)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
308
|
+
for job in jobs:
|
|
309
|
+
ran_job = True
|
|
310
|
+
result, output = _run_with_retries(
|
|
311
|
+
build_act_command("workflow_dispatch", workflow=workflow, job=job, arch=arch),
|
|
312
|
+
retries,
|
|
313
|
+
backoff,
|
|
314
|
+
)
|
|
315
|
+
if result == PreflightResult.UNREACHABLE:
|
|
316
|
+
return PreflightResult.UNREACHABLE
|
|
317
|
+
# We KNOW this job exists (it came from `act --list`), so "no stages"
|
|
318
|
+
# here means act ran nothing — a failure, not a no-op. Do not pass.
|
|
319
|
+
if result != PreflightResult.PASS or _has_no_stages(output):
|
|
320
|
+
return PreflightResult.FAIL
|
|
321
|
+
|
|
322
|
+
if not ran_job:
|
|
323
|
+
sys.stdout.write(
|
|
324
|
+
"ℹ️ CI/security dispatch workflows contain no runnable jobs — "
|
|
325
|
+
"nothing to validate; treating as pass.\n"
|
|
280
326
|
)
|
|
281
|
-
if result == PreflightResult.UNREACHABLE:
|
|
282
|
-
return PreflightResult.UNREACHABLE
|
|
283
|
-
# We KNOW this job exists (it came from `act --list`), so "no stages"
|
|
284
|
-
# here means act ran nothing — a failure, not a no-op. Do not pass.
|
|
285
|
-
if result != PreflightResult.PASS or _has_no_stages(output):
|
|
286
|
-
return PreflightResult.FAIL
|
|
287
327
|
|
|
288
328
|
return PreflightResult.PASS
|
|
289
329
|
|
|
290
330
|
|
|
291
|
-
def preflight(
|
|
331
|
+
def preflight(
|
|
332
|
+
repo_slug: str, event: str = "pull_request", lock_timeout: float | None = None
|
|
333
|
+
) -> PreflightResult:
|
|
292
334
|
"""Primary gate. PASS/FAIL if act can run; UNREACHABLE if act can't start here.
|
|
293
335
|
|
|
294
336
|
`repo_slug` is accepted for symmetry and future use; the runner fallback is
|
|
@@ -296,10 +338,28 @@ def preflight(repo_slug: str, event: str = "pull_request") -> PreflightResult:
|
|
|
296
338
|
|
|
297
339
|
Prefers a repo-defined native gate (`make ci-native`) over act: it runs the
|
|
298
340
|
same checks on the host toolchain, avoiding act's flaky container emulation.
|
|
341
|
+
|
|
342
|
+
The act run — and only the act run — is serialised machine-wide by
|
|
343
|
+
`act_lock`, because every repo's act run drives the same container runtime.
|
|
344
|
+
The native gate is host-local and stays unlocked, and `probe()` runs before
|
|
345
|
+
the wait so a dead docker is UNREACHABLE immediately instead of after
|
|
346
|
+
`lock_timeout` seconds. On lock timeout the gate reports UNREACHABLE (the
|
|
347
|
+
established "cannot validate" state) rather than starting the second
|
|
348
|
+
concurrent container run this lock exists to prevent.
|
|
299
349
|
"""
|
|
300
350
|
if native_gate_available():
|
|
301
351
|
sys.stdout.write("▶ Native CI gate found — running `make ci-native` (no act)…\n")
|
|
302
352
|
return run_native()
|
|
303
353
|
if not probe():
|
|
304
354
|
return PreflightResult.UNREACHABLE
|
|
305
|
-
|
|
355
|
+
with act_lock(timeout=lock_timeout) as acquired:
|
|
356
|
+
if not acquired:
|
|
357
|
+
sys.stdout.write(
|
|
358
|
+
f"⚠️ Timed out after {resolve_timeout(lock_timeout):.0f}s waiting for the "
|
|
359
|
+
"machine-wide act lock — another `mcli ci preflight` is still running act. "
|
|
360
|
+
"Refusing to start a second concurrent container run; this push was NOT "
|
|
361
|
+
"validated locally (re-run `mcli ci preflight` when the machine is idle, "
|
|
362
|
+
f"or raise the wait with --lock-timeout / {LOCK_TIMEOUT_ENV}).\n"
|
|
363
|
+
)
|
|
364
|
+
return PreflightResult.UNREACHABLE
|
|
365
|
+
return run_act(event)
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
|
|
12
|
+
from mcli.workflow.ci.act_lock import DEFAULT_LOCK_TIMEOUT_SECONDS, LOCK_TIMEOUT_ENV
|
|
12
13
|
from mcli.workflow.ci.act_runner import PreflightResult, act_available, docker_running
|
|
13
14
|
from mcli.workflow.ci.act_runner import preflight as preflight_fn
|
|
14
15
|
from mcli.workflow.ci.runner_status import has_online_runner
|
|
@@ -98,10 +99,21 @@ def migrate(dry_run):
|
|
|
98
99
|
|
|
99
100
|
@ci.command()
|
|
100
101
|
@click.option("--event", default="pull_request", show_default=True, help="act event to simulate.")
|
|
101
|
-
|
|
102
|
+
@click.option(
|
|
103
|
+
"--lock-timeout",
|
|
104
|
+
type=float,
|
|
105
|
+
default=None,
|
|
106
|
+
help=(
|
|
107
|
+
"Seconds to wait for the machine-wide act lock before giving up "
|
|
108
|
+
f"(default {DEFAULT_LOCK_TIMEOUT_SECONDS:.0f}s, or ${LOCK_TIMEOUT_ENV}). "
|
|
109
|
+
"act runs are serialised across repos so concurrent pushes cannot "
|
|
110
|
+
"starve the shared container runtime."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
def preflight(event, lock_timeout):
|
|
102
114
|
"""Run act as the PR gate. Exit 0=pass, 1=fail, 2=cannot validate, 3=use runner."""
|
|
103
115
|
slug = current_repo_slug()
|
|
104
|
-
result = preflight_fn(slug, event)
|
|
116
|
+
result = preflight_fn(slug, event, lock_timeout=lock_timeout)
|
|
105
117
|
if result == PreflightResult.PASS:
|
|
106
118
|
click.echo("✅ act passed — OK to open PR.")
|
|
107
119
|
raise SystemExit(0)
|
|
@@ -146,8 +158,25 @@ PRE_PUSH_HOOK = """#!/usr/bin/env bash
|
|
|
146
158
|
# Blocks ONLY on a real act failure (exit 1). When act cannot validate locally
|
|
147
159
|
# — e.g. Docker Hub rate-limit (toomanyrequests, retried first), no docker, or
|
|
148
160
|
# an online runner will validate instead (exit 2/3) — the push is allowed so an
|
|
149
|
-
# environment hiccup never wedges your workflow.
|
|
161
|
+
# environment hiccup never wedges your workflow.
|
|
162
|
+
# act runs are serialised machine-wide, so this may wait for another repo's
|
|
163
|
+
# preflight to finish (bounded; see --lock-timeout / MCLI_CI_LOCK_TIMEOUT).
|
|
164
|
+
# Override a real failure with:
|
|
150
165
|
# git push --no-verify
|
|
166
|
+
#
|
|
167
|
+
# A gate that cannot run must not pass. Git hooks inherit the environment of
|
|
168
|
+
# whatever invoked `git`, which is not necessarily a login shell — an IDE, a
|
|
169
|
+
# GUI client or a cron job can push with a PATH that never sourced your
|
|
170
|
+
# profile. Without the check below, `mcli` would simply be "command not found"
|
|
171
|
+
# (exit 127), the `-eq 1` test would not match, and the push would sail through
|
|
172
|
+
# with nothing validated — indistinguishable from having no hook at all.
|
|
173
|
+
if ! command -v mcli >/dev/null 2>&1; then
|
|
174
|
+
echo "mcli-ci: mcli is not on PATH, so nothing was validated — push blocked." >&2
|
|
175
|
+
echo " This shell's PATH: $PATH" >&2
|
|
176
|
+
echo " Fix the PATH (hooks do not source your shell profile), or override:" >&2
|
|
177
|
+
echo " git push --no-verify" >&2
|
|
178
|
+
exit 1
|
|
179
|
+
fi
|
|
151
180
|
mcli ci preflight
|
|
152
181
|
code=$?
|
|
153
182
|
if [ "$code" -eq 1 ]; then
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcli-framework
|
|
3
|
-
Version: 8.0.
|
|
3
|
+
Version: 8.0.65
|
|
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>
|
|
@@ -163,6 +163,7 @@ src/mcli/workflow/doc_convert.py
|
|
|
163
163
|
src/mcli/workflow/lsh_integration.py
|
|
164
164
|
src/mcli/workflow/workflow.py
|
|
165
165
|
src/mcli/workflow/ci/__init__.py
|
|
166
|
+
src/mcli/workflow/ci/act_lock.py
|
|
166
167
|
src/mcli/workflow/ci/act_runner.py
|
|
167
168
|
src/mcli/workflow/ci/ci.py
|
|
168
169
|
src/mcli/workflow/ci/runner_status.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
|