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.
Files changed (239) hide show
  1. {mcli_framework-8.0.62/src/mcli_framework.egg-info → mcli_framework-8.0.65}/PKG-INFO +1 -1
  2. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/pyproject.toml +1 -1
  3. mcli_framework-8.0.65/src/mcli/workflow/ci/act_lock.py +171 -0
  4. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/act_runner.py +114 -54
  5. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/ci.py +32 -3
  6. {mcli_framework-8.0.62 → mcli_framework-8.0.65/src/mcli_framework.egg-info}/PKG-INFO +1 -1
  7. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/SOURCES.txt +1 -0
  8. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/LICENSE +0 -0
  9. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/MANIFEST.in +0 -0
  10. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/README.md +0 -0
  11. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/llms-full.txt +0 -0
  12. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/llms.txt +0 -0
  13. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/Cargo.toml +0 -0
  14. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/command_parser.rs +0 -0
  15. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/file_watcher.rs +0 -0
  16. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/lib.rs +0 -0
  17. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/process_manager.rs +0 -0
  18. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/mcli_rust/src/tfidf.rs +0 -0
  19. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/setup.cfg +0 -0
  20. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/__init__.py +0 -0
  21. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/__main__.py +0 -0
  22. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/__init__.py +0 -0
  23. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/completion_helpers.py +0 -0
  24. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/context_cmd.py +0 -0
  25. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/delete_cmd.py +0 -0
  26. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/edit_cmd.py +0 -0
  27. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/import_cmd.py +0 -0
  28. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/init_cmd.py +0 -0
  29. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/list_cmd.py +0 -0
  30. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/main.py +0 -0
  31. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/model/__init__.py +0 -0
  32. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/model/model.py +0 -0
  33. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/model_cmd.py +0 -0
  34. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/mv_cmd.py +0 -0
  35. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/new_cmd.py +0 -0
  36. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/search_cmd.py +0 -0
  37. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/services_cmd.py +0 -0
  38. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/setup_cmd.py +0 -0
  39. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/source_sync_cmd.py +0 -0
  40. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/sync_cmd.py +0 -0
  41. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/video/__init__.py +0 -0
  42. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/app/video/video.py +0 -0
  43. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/config.toml +0 -0
  44. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/__init__.py +0 -0
  45. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/__init__.py +0 -0
  46. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/api.py +0 -0
  47. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/daemon_client.py +0 -0
  48. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/daemon_client_local.py +0 -0
  49. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/daemon_decorator.py +0 -0
  50. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/api/mcli_decorators.py +0 -0
  51. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/__init__.py +0 -0
  52. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/auth.py +0 -0
  53. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/aws_manager.py +0 -0
  54. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/azure_manager.py +0 -0
  55. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/credential_manager.py +0 -0
  56. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/gcp_manager.py +0 -0
  57. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/key_manager.py +0 -0
  58. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/mcli_manager.py +0 -0
  59. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/token_manager.py +0 -0
  60. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/auth/token_util.py +0 -0
  61. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/config/__init__.py +0 -0
  62. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/config/config.py +0 -0
  63. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/config/settings.py +0 -0
  64. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/__init__.py +0 -0
  65. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/commands.py +0 -0
  66. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/defaults.py +0 -0
  67. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/env.py +0 -0
  68. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/messages.py +0 -0
  69. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/paths.py +0 -0
  70. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/scripts.py +0 -0
  71. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/constants/storage.py +0 -0
  72. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/custom_commands.py +0 -0
  73. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/discovery/__init__.py +0 -0
  74. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/discovery/command_discovery.py +0 -0
  75. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/erd/__init__.py +0 -0
  76. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/erd/erd.py +0 -0
  77. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/erd/generate_graph.py +0 -0
  78. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/errors.py +0 -0
  79. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/feature_detection.py +0 -0
  80. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/files/__init__.py +0 -0
  81. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/files/files.py +0 -0
  82. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/folder_workflows.py +0 -0
  83. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/fs/__init__.py +0 -0
  84. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/fs/fs.py +0 -0
  85. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ipfs_sync.py +0 -0
  86. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ipfs_utils.py +0 -0
  87. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ipns_manager.py +0 -0
  88. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/lib.py +0 -0
  89. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/__init__.py +0 -0
  90. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/correlation.py +0 -0
  91. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/logger.py +0 -0
  92. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/logger/structured.py +0 -0
  93. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/optional_deps.py +0 -0
  94. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/paths.py +0 -0
  95. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/__init__.py +0 -0
  96. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/optimizer.py +0 -0
  97. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/rust_bridge.py +0 -0
  98. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/performance/uvloop_config.py +0 -0
  99. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pickles/__init__.py +0 -0
  100. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pickles/pickles.py +0 -0
  101. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/__init__.py +0 -0
  102. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/deps.py +0 -0
  103. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/manager.py +0 -0
  104. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/pyenv/venv.py +0 -0
  105. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/script_loader.py +0 -0
  106. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/script_sync.py +0 -0
  107. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/script_watcher.py +0 -0
  108. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/search/cached_vectorizer.py +0 -0
  109. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/__init__.py +0 -0
  110. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/commands.py +0 -0
  111. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/manager.py +0 -0
  112. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/repl.py +0 -0
  113. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/secrets/store.py +0 -0
  114. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/__init__.py +0 -0
  115. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/config.py +0 -0
  116. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/data_pipeline.py +0 -0
  117. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/health.py +0 -0
  118. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/lsh_client.py +0 -0
  119. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/manager.py +0 -0
  120. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/redis_service.py +0 -0
  121. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/registry.py +0 -0
  122. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/state.py +0 -0
  123. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/services/supervisor.py +0 -0
  124. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/shell/__init__.py +0 -0
  125. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/shell/exceptions.py +0 -0
  126. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/shell/shell.py +0 -0
  127. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/sync_key_store.py +0 -0
  128. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/templates/__init__.py +0 -0
  129. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/templates/command_templates.py +0 -0
  130. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/toml/__init__.py +0 -0
  131. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/toml/toml.py +0 -0
  132. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/types.py +0 -0
  133. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ui/styling.py +0 -0
  134. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/ui/visual_effects.py +0 -0
  135. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/watcher/__init__.py +0 -0
  136. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/watcher/watcher.py +0 -0
  137. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/workflow_models.py +0 -0
  138. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/workflow_runtime.py +0 -0
  139. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/lib/workspace_registry.py +0 -0
  140. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/mygroup/__init__.py +0 -0
  141. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/mygroup/test_cmd.py +0 -0
  142. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/__init__.py +0 -0
  143. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/commands/__init__.py +0 -0
  144. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/oi/oi.py +0 -0
  145. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/public/public.py +0 -0
  146. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/__init__.py +0 -0
  147. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/completion_cmd.py +0 -0
  148. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/env_cmd.py +0 -0
  149. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/health_cmd.py +0 -0
  150. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/ipfs_cmd.py +0 -0
  151. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/logs_cmd.py +0 -0
  152. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/migrate_cmd.py +0 -0
  153. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/release_notes_cmd.py +0 -0
  154. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/self_cmd.py +0 -0
  155. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/store_cmd.py +0 -0
  156. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/test_cmd.py +0 -0
  157. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/self/workflows_cmd.py +0 -0
  158. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/__init__.py +0 -0
  159. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/backends/__init__.py +0 -0
  160. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/backends/ipfs_backend.py +0 -0
  161. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/base.py +0 -0
  162. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/cache.py +0 -0
  163. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/encryption.py +0 -0
  164. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/factory.py +0 -0
  165. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/registry.py +0 -0
  166. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/storage/storacha_cli.py +0 -0
  167. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/__init__.py +0 -0
  168. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/__init__.py +0 -0
  169. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/runner_status.py +0 -0
  170. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/ci/workflow_transform.py +0 -0
  171. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/__init__.py +0 -0
  172. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/async_command_database.py +0 -0
  173. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/async_process_manager.py +0 -0
  174. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/client.py +0 -0
  175. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/daemon.py +0 -0
  176. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/daemon_api.py +0 -0
  177. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/enhanced_daemon.py +0 -0
  178. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/process_cli.py +0 -0
  179. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/process_manager.py +0 -0
  180. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/daemon/test_daemon.py +0 -0
  181. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/doc_convert.py +0 -0
  182. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/docker/__init__.py +0 -0
  183. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/docker/docker.py +0 -0
  184. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/file/__init__.py +0 -0
  185. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/gcloud/__init__.py +0 -0
  186. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/gcloud/config.toml +0 -0
  187. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/gcloud/gcloud.py +0 -0
  188. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/git_commit/__init__.py +0 -0
  189. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/git_commit/ai_service.py +0 -0
  190. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/interview/__init__.py +0 -0
  191. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/lsh_integration.py +0 -0
  192. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/client.py +0 -0
  193. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/download_and_run_efficient_models.py +0 -0
  194. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/lightweight_embedder.py +0 -0
  195. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/lightweight_model_server.py +0 -0
  196. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/lightweight_test.py +0 -0
  197. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/model_service.py +0 -0
  198. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/ollama_efficient_runner.py +0 -0
  199. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/openai_adapter.py +0 -0
  200. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/pdf_processor.py +0 -0
  201. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_efficient_runner.py +0 -0
  202. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_example.py +0 -0
  203. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_integration.py +0 -0
  204. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/model_service/test_new_features.py +0 -0
  205. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/__init__.py +0 -0
  206. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/command_loader.py +0 -0
  207. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/converter.py +0 -0
  208. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/executor.py +0 -0
  209. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/notebook_cmd.py +0 -0
  210. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/schema.py +0 -0
  211. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/notebook/validator.py +0 -0
  212. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/openai/openai.py +0 -0
  213. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/registry/__init__.py +0 -0
  214. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/registry/registry.py +0 -0
  215. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/repo/__init__.py +0 -0
  216. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/repo/repo.py +0 -0
  217. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/__init__.py +0 -0
  218. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/cron_parser.py +0 -0
  219. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/job.py +0 -0
  220. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/models.py +0 -0
  221. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/monitor.py +0 -0
  222. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/persistence.py +0 -0
  223. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/scheduler.py +0 -0
  224. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/scheduler/validation.py +0 -0
  225. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/search/__init__.py +0 -0
  226. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/secrets/__init__.py +0 -0
  227. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/secrets/secrets_cmd.py +0 -0
  228. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/storage/__init__.py +0 -0
  229. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/storage/storage_cmd.py +0 -0
  230. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/sync/__init__.py +0 -0
  231. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/sync/test_cmd.py +0 -0
  232. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/videos/__init__.py +0 -0
  233. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/wakatime/__init__.py +0 -0
  234. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/wakatime/wakatime.py +0 -0
  235. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli/workflow/workflow.py +0 -0
  236. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/dependency_links.txt +0 -0
  237. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/entry_points.txt +0 -0
  238. {mcli_framework-8.0.62 → mcli_framework-8.0.65}/src/mcli_framework.egg-info/requires.txt +0 -0
  239. {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.62
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.62"
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
- # Canonical CI entrypoint created by `mcli ci migrate`: ci.yml runs on
32
- # `workflow_dispatch`. The job id is NOT fixed (`mcli ci migrate` keeps the
33
- # repo's original job name, e.g. `test`) — so preflight discovers the real job
34
- # id(s) from `act --list` rather than assuming a hardcoded name.
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(["act", "-l"], capture_output=True, text=True, timeout=60)
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
- act's runner images are amd64. On Apple-silicon (darwin/arm64) act warns and
111
- misbehaves unless told to emulate amd64, so force it; native amd64 Linux
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/amd64"
121
+ return "linux/arm64"
116
122
  return None
117
123
 
118
124
 
119
- def dispatch_workflow() -> str | None:
120
- """The canonical workflow file to drive via `workflow_dispatch`, or None."""
121
- if _DISPATCH_WORKFLOW.exists():
122
- return str(_DISPATCH_WORKFLOW)
123
- return None
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) or when act can't be invoked — never
133
- raises, so callers can treat an empty result as "nothing to run".
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 = ["act", event, "--list"]
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 = ["act", event]
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, fall back
238
- to the canonical ``ci.yml`` on ``workflow_dispatch``: discover its real job
239
- id(s) via ``act --list`` (the job name is NOT fixed — ``mcli ci migrate``
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 the workflow_dispatch entrypoint.
253
- workflow = dispatch_workflow()
254
- if workflow is None:
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 ci.yml dispatch "
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
- jobs = list_jobs("workflow_dispatch", workflow)
262
- if not jobs:
263
- # The dispatch entrypoint genuinely has no jobs a real no-op.
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"ℹ️ {workflow} has no workflow_dispatch jobs — nothing to "
266
- "validate; treating as pass.\n"
305
+ f"ℹ️ No '{event}' stages (workflow_dispatch-only); running "
306
+ f"{workflow} {jobs} via workflow_dispatch…\n"
267
307
  )
268
- return PreflightResult.PASS
269
-
270
- targets = f"{workflow} {jobs}"
271
- sys.stdout.write(
272
- f"ℹ️ No '{event}' stages (workflow_dispatch-only); running "
273
- f"{targets} via workflow_dispatch…\n"
274
- )
275
- for job in jobs:
276
- result, output = _run_with_retries(
277
- build_act_command("workflow_dispatch", workflow=workflow, job=job, arch=arch),
278
- retries,
279
- backoff,
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(repo_slug: str, event: str = "pull_request") -> PreflightResult:
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
- return run_act(event)
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
- def preflight(event):
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. Override a real failure with:
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.62
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