framework-m-studio 0.8.2__tar.gz → 0.8.4__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 (165) hide show
  1. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/CHANGELOG.md +35 -0
  2. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/PKG-INFO +3 -3
  3. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/pyproject.toml +11 -4
  4. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/deps.py +69 -16
  5. framework_m_studio-0.8.4/src/framework_m_studio/cli/release/__init__.py +76 -0
  6. framework_m_studio-0.8.4/src/framework_m_studio/cli/release/cli.py +201 -0
  7. framework_m_studio-0.8.4/src/framework_m_studio/cli/release/gitops.py +194 -0
  8. framework_m_studio-0.8.4/src/framework_m_studio/cli/release/models.py +76 -0
  9. framework_m_studio-0.8.4/src/framework_m_studio/cli/release/orchestrator.py +389 -0
  10. framework_m_studio-0.8.4/src/framework_m_studio/cli/release/versioning.py +172 -0
  11. framework_m_studio-0.8.2/src/framework_m_studio/static/assets/index-BT9-FyVu.css → framework_m_studio-0.8.4/src/framework_m_studio/static/assets/index-BXzUquLi.css +1 -1
  12. framework_m_studio-0.8.2/src/framework_m_studio/static/assets/index-D11hOvMw.js → framework_m_studio-0.8.4/src/framework_m_studio/static/assets/index-C7HXLlUt.js +52 -52
  13. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/static/index.html +2 -2
  14. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/package.json +1 -1
  15. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/package.json.template +4 -4
  16. framework_m_studio-0.8.4/studio_ui/CHANGELOG.md +37 -0
  17. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/package.json +13 -13
  18. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_release_cli.py +271 -74
  19. framework_m_studio-0.8.2/src/framework_m_studio/cli/release.py +0 -888
  20. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/.gitignore +0 -0
  21. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/LICENSE +0 -0
  22. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/README.md +0 -0
  23. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/docs/api/index.md +0 -0
  24. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/docs/user-guide.md +0 -0
  25. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/__init__.py +0 -0
  26. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/app.py +0 -0
  27. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/__init__.py +0 -0
  28. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/codegen.py +0 -0
  29. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/constants.py +0 -0
  30. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/contrib.py +0 -0
  31. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/dev.py +0 -0
  32. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/docs.py +0 -0
  33. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/new.py +0 -0
  34. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/quality.py +0 -0
  35. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/studio.py +0 -0
  36. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/utility.py +0 -0
  37. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/verify.py +0 -0
  38. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/__init__.py +0 -0
  39. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/generator.py +0 -0
  40. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/parser.py +0 -0
  41. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/templates/doctype.py.jinja2 +0 -0
  42. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/templates/test_doctype.py.jinja2 +0 -0
  43. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/test_generator.py +0 -0
  44. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/transformer.py +0 -0
  45. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/core/frontend.py +0 -0
  46. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/core/gitops.py +0 -0
  47. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/core/shell.py +0 -0
  48. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/discovery.py +0 -0
  49. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/ast_parsers.py +0 -0
  50. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/audit.py +0 -0
  51. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/exporters.py +0 -0
  52. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/formatters.py +0 -0
  53. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/generator.py +0 -0
  54. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/openapi.py +0 -0
  55. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/__init__.py +0 -0
  56. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/adapter.py +0 -0
  57. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/github_provider.py +0 -0
  58. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/protocol.py +0 -0
  59. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/protocol_scanner.py +0 -0
  60. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/py.typed +0 -0
  61. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/routes.py +0 -0
  62. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/sdk_generator.py +0 -0
  63. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/static/.gitkeep +0 -0
  64. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/static/favicon.ico +0 -0
  65. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/.gitignore +0 -0
  66. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/.npmrc +0 -0
  67. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/README.md +0 -0
  68. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/index.html +0 -0
  69. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/App.tsx +0 -0
  70. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/components/AppNavbar.tsx +0 -0
  71. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/components/AppSidebar.tsx +0 -0
  72. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/index.tsx +0 -0
  73. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/pages/LoginPage.tsx +0 -0
  74. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/theme.css +0 -0
  75. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/tsconfig.json +0 -0
  76. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/vite.config.ts +0 -0
  77. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/Dashboard.tsx.template +0 -0
  78. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/README.md.template +0 -0
  79. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/plugin.config.ts.template +0 -0
  80. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/tsconfig.json.template +0 -0
  81. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/vite.config.mfe.ts.template +0 -0
  82. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/workspace.py +0 -0
  83. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/.gitignore +0 -0
  84. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/.npmrc +0 -0
  85. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/Dockerfile +0 -0
  86. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/README.md +0 -0
  87. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/eslint.config.js +0 -0
  88. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/index.html +0 -0
  89. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/pnpm-lock.yaml +0 -0
  90. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/App.css +0 -0
  91. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/App.test.tsx +0 -0
  92. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/App.tsx +0 -0
  93. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/AutoForm.tsx +0 -0
  94. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/AutoTable.tsx +0 -0
  95. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/CodePreview.tsx +0 -0
  96. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/ControllerEditor.tsx +0 -0
  97. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/ERDView.tsx +0 -0
  98. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/FieldEditor.tsx +0 -0
  99. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/LayoutDesigner.tsx +0 -0
  100. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/ModuleExplorer.tsx +0 -0
  101. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/NestedEditorDrawer.tsx +0 -0
  102. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/SandboxPreview.tsx +0 -0
  103. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/TableFieldEditor.tsx +0 -0
  104. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/breadcrumb/index.tsx +0 -0
  105. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/fields/DefaultFieldComponents.tsx +0 -0
  106. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/fields/index.ts +0 -0
  107. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/fields/resolveFieldComponent.ts +0 -0
  108. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/layout/index.tsx +0 -0
  109. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/menu/index.tsx +0 -0
  110. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/index.css +0 -0
  111. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/index.tsx +0 -0
  112. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/doctypes/edit.tsx +0 -0
  113. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/doctypes/index.ts +0 -0
  114. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/doctypes/list.tsx +0 -0
  115. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/index.ts +0 -0
  116. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/ThemeContext.ts +0 -0
  117. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/constants.ts +0 -0
  118. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/data.ts +0 -0
  119. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/theme.tsx +0 -0
  120. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/useTheme.ts +0 -0
  121. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/registry/defaultComponents.ts +0 -0
  122. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/registry/fieldComponents.ts +0 -0
  123. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/registry/index.ts +0 -0
  124. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/services/mock_data.ts +0 -0
  125. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/setupTests.ts +0 -0
  126. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/utils/mockDataGenerator.ts +0 -0
  127. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/vite-env.d.ts +0 -0
  128. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/tsconfig.json +0 -0
  129. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/tsconfig.node.json +0 -0
  130. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/vite.config.ts +0 -0
  131. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/vitest.config.ts +0 -0
  132. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/cli/test_new.py +0 -0
  133. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/cli/test_verify.py +0 -0
  134. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/conftest.py +0 -0
  135. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/core/test_frontend.py +0 -0
  136. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/core/test_gitops.py +0 -0
  137. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/core/test_shell.py +0 -0
  138. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_api_routes.py +0 -0
  139. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_cli.py +0 -0
  140. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_cli_openapi.py +0 -0
  141. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_contrib_cli.py +0 -0
  142. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_deps_cli.py +0 -0
  143. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_dev.py +0 -0
  144. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_dev_cli_coverage.py +0 -0
  145. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_discovery.py +0 -0
  146. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_docs_audit.py +0 -0
  147. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_docs_generator.py +0 -0
  148. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_docs_generator_enhanced.py +0 -0
  149. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_frontend_integration.py +0 -0
  150. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_generator.py +0 -0
  151. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_git_adapter.py +0 -0
  152. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_git_protocol.py +0 -0
  153. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_github_provider.py +0 -0
  154. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_new_cli.py +0 -0
  155. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_parser.py +0 -0
  156. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_protocol_scanner.py +0 -0
  157. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_quality_cli.py +0 -0
  158. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_screenshot_pipeline.py +0 -0
  159. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_sdk_generator.py +0 -0
  160. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_studio.py +0 -0
  161. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_studio_codegen_generator.py +0 -0
  162. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_transformer.py +0 -0
  163. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_utility_cli.py +0 -0
  164. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_utility_cli_enhanced.py +0 -0
  165. {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_workspace.py +0 -0
@@ -1,3 +1,38 @@
1
+ ## framework-m-studio v0.8.4
2
+
3
+ ### Bug Fixes
4
+
5
+ - implement bulk dependency upgrades and fix typecheck stubs (bf01d94)
6
+ - install pnpm and coreutils in release phase (ed335f8)
7
+ - enforce tool availability and install pnpm in CI (b8f9465)
8
+ - enforce lockfile sync, stage templates, and fix types (a41aad0)
9
+ - enhance local dry-run and silence check-ignore spam (29f9ef6)
10
+ - modernize gitops auth and api integration (7f15453)
11
+ - secure staging logic and resolve test regressions (e31d701)
12
+ - implement pnpm/uv lockfile sync and config cleanup (8302956)
13
+ - lint cleanup for cli.py (40b3a78)
14
+ - support GITLAB_TOKEN and harden auth check (c08b25c)
15
+ - implement atomic direct push for CI/CD stability (2947270)
16
+ - harden sync orchestration and align warning filters (714f9ba)
17
+ - modularize release package and resolve test regressions (fe8c676)
18
+
19
+ ## framework-m-studio v0.8.3
20
+
21
+ ### Bug Fixes
22
+
23
+ - install pnpm and coreutils in release phase (ed335f8)
24
+ - enforce tool availability and install pnpm in CI (b8f9465)
25
+ - enforce lockfile sync, stage templates, and fix types (a41aad0)
26
+ - enhance local dry-run and silence check-ignore spam (29f9ef6)
27
+ - modernize gitops auth and api integration (7f15453)
28
+ - secure staging logic and resolve test regressions (e31d701)
29
+ - implement pnpm/uv lockfile sync and config cleanup (8302956)
30
+ - lint cleanup for cli.py (40b3a78)
31
+ - support GITLAB_TOKEN and harden auth check (c08b25c)
32
+ - implement atomic direct push for CI/CD stability (2947270)
33
+ - harden sync orchestration and align warning filters (714f9ba)
34
+ - modularize release package and resolve test regressions (fe8c676)
35
+
1
36
  # Changelog
2
37
 
3
38
  ## framework-m-studio v0.8.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: framework-m-studio
3
- Version: 0.8.2
3
+ Version: 0.8.4
4
4
  Summary: Framework M Studio - Visual DocType Builder & Developer Tools
5
5
  Project-URL: Homepage, https://gitlab.com/castlecraft/framework-m
6
6
  Project-URL: Documentation, https://gitlab.com/castlecraft/framework-m#readme
@@ -20,8 +20,8 @@ Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.12
21
21
  Requires-Dist: bandit>=1.9.4
22
22
  Requires-Dist: build>=1.0.0
23
- Requires-Dist: framework-m-core>=0.11.2
24
- Requires-Dist: framework-m>=0.6.6
23
+ Requires-Dist: framework-m-core>=0.11.4
24
+ Requires-Dist: framework-m>=0.6.8
25
25
  Requires-Dist: honcho>=1.1.0
26
26
  Requires-Dist: httpx>=0.27.0
27
27
  Requires-Dist: ipython>=9.11.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "framework-m-studio"
3
- version = "0.8.2"
3
+ version = "0.8.4"
4
4
  description = "Framework M Studio - Visual DocType Builder & Developer Tools"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -21,8 +21,8 @@ classifiers = [
21
21
  ]
22
22
 
23
23
  dependencies = [
24
- "framework-m-core>=0.11.2",
25
- "framework-m>=0.6.6",
24
+ "framework-m-core>=0.11.4",
25
+ "framework-m>=0.6.8",
26
26
  # Code generation
27
27
  "libcst>=1.0.0",
28
28
  "jinja2>=3.1.0",
@@ -116,7 +116,10 @@ asyncio_mode = "auto"
116
116
  testpaths = ["tests"]
117
117
  addopts = "-v --tb=short"
118
118
  filterwarnings = [
119
- "ignore:Core Pydantic V1 functionality isn't compatible with Python .* or greater.:UserWarning",
119
+ "ignore::DeprecationWarning",
120
+ "ignore::pytest.PytestRemovedIn9Warning",
121
+ "ignore:Core Pydantic V1 functionality isn't compatible.*:UserWarning:litestar.*",
122
+ "ignore:unclosed database in <sqlite3.Connection.*:ResourceWarning",
120
123
  ]
121
124
 
122
125
  [tool.coverage.run]
@@ -145,6 +148,10 @@ ignore_missing_imports = true
145
148
  module = "honcho.*"
146
149
  ignore_missing_imports = true
147
150
 
151
+ [[tool.mypy.overrides]]
152
+ module = "yaml"
153
+ ignore_missing_imports = true
154
+
148
155
  [tool.ruff]
149
156
  line-length = 88
150
157
  target-version = "py312"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import sys
8
+ from datetime import datetime
8
9
  from pathlib import Path
9
10
  from typing import Annotated
10
11
 
@@ -94,7 +95,10 @@ def deps_check_command(
94
95
 
95
96
  @deps_app.command(name="upgrade")
96
97
  async def deps_upgrade_command(
97
- package: str,
98
+ package: Annotated[
99
+ str | None,
100
+ cyclopts.Parameter(name="package", help="Package to upgrade (omit for all)"),
101
+ ] = None,
98
102
  manager: Annotated[
99
103
  str, cyclopts.Parameter(name="--manager", help="uv or pnpm")
100
104
  ] = "uv",
@@ -113,6 +117,12 @@ async def deps_upgrade_command(
113
117
  root: Annotated[
114
118
  str | None, cyclopts.Parameter(name="--root", help="Project root directory")
115
119
  ] = None,
120
+ skip_branch: Annotated[
121
+ bool,
122
+ cyclopts.Parameter(
123
+ name="--skip-branch", help="Skip branch creation, keep changes unstaged"
124
+ ),
125
+ ] = False,
116
126
  ) -> None:
117
127
  """Upgrade a dependency and create a merge request."""
118
128
  root_path = Path(root) if root else Path.cwd()
@@ -124,30 +134,73 @@ async def deps_upgrade_command(
124
134
  )
125
135
  final_project_id = project_id or os.environ.get("CI_PROJECT_ID") or ""
126
136
 
127
- if manager == "uv":
128
- run_cmd(f"uv lock --upgrade-package {package}", cwd=root_path)
129
- elif manager == "pnpm":
130
- run_cmd(f"pnpm update -r {package}", cwd=root_path)
137
+ # Check for changes in lockfiles and git status
138
+ uv_lock = root_path / "uv.lock"
139
+ pnpm_lock = root_path / "pnpm-lock.yaml"
140
+
141
+ uv_mtime = uv_lock.stat().st_mtime if uv_lock.exists() else 0
142
+ pnpm_mtime = pnpm_lock.stat().st_mtime if pnpm_lock.exists() else 0
143
+
144
+ if not package:
145
+ print(f"Checking for all outdated packages using {manager}...")
146
+ if manager == "uv":
147
+ outdated = _check_uv_outdated(root_path)
148
+ packages_to_upgrade = [pkg["name"] for pkg in outdated]
149
+ if not packages_to_upgrade:
150
+ print("All uv dependencies are up to date.")
151
+ return
152
+
153
+ print(f"Upgrading {len(packages_to_upgrade)} packages...")
154
+ pkg_args = " ".join(
155
+ [f"--upgrade-package {pkg}" for pkg in packages_to_upgrade]
156
+ )
157
+ run_cmd(f"uv lock {pkg_args}", cwd=root_path)
158
+ elif manager == "pnpm":
159
+ run_cmd("pnpm update -r", cwd=root_path)
160
+ print("Running pnpm audit fix...")
161
+ run_cmd("pnpm audit fix", cwd=root_path, check=False)
162
+ else:
163
+ print(f"Unknown manager: {manager}", file=sys.stderr)
164
+ return
165
+ package = "all"
131
166
  else:
132
- print(f"Unknown manager: {manager}", file=sys.stderr)
167
+ if manager == "uv":
168
+ run_cmd(f"uv lock --upgrade-package {package}", cwd=root_path)
169
+ elif manager == "pnpm":
170
+ run_cmd(f"pnpm update -r {package}", cwd=root_path)
171
+ else:
172
+ print(f"Unknown manager: {manager}", file=sys.stderr)
173
+ return
174
+
175
+ # Check if anything actually changed
176
+ new_uv_mtime = uv_lock.stat().st_mtime if uv_lock.exists() else 0
177
+ new_pnpm_mtime = pnpm_lock.stat().st_mtime if pnpm_lock.exists() else 0
178
+
179
+ lock_changed = (new_uv_mtime != uv_mtime) or (new_pnpm_mtime != pnpm_mtime)
180
+ git_status = run_cmd("git status --short", cwd=root_path)
181
+
182
+ if not (git_status or lock_changed):
183
+ print(f"No changes found after updating {package or 'all'}. Skipping MR.")
133
184
  return
134
185
 
135
- if dry_run:
136
- print(f"[DRY RUN] Would create MR for {package}")
186
+ if dry_run or skip_branch:
187
+ if dry_run:
188
+ print(f"[DRY RUN] Would create MR for {package or 'all'}")
137
189
  return
138
190
 
139
- safe_name = package.replace("@", "").replace("/", "-")
140
- branch_name = f"deps/{manager}-{safe_name}"
191
+ # Create and switch to new branch
192
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
193
+ branch_name = f"deps/{manager}-{package or 'all'}-{timestamp}"
141
194
 
142
- run_cmd("git checkout main", cwd=root_path, check=False)
195
+ # Check for existing branch and delete it
143
196
  run_cmd(f"git branch -D {branch_name}", cwd=root_path, check=False)
144
197
  run_cmd(f"git checkout -b {branch_name}", cwd=root_path)
145
198
 
146
- status = run_cmd("git status --short", cwd=root_path)
147
- if not status:
148
- print(f"No changes found after updating {package}. Skipping MR.")
149
- run_cmd("git checkout main", cwd=root_path)
150
- return
199
+ # Force add lockfiles if they are ignored
200
+ if manager == "uv":
201
+ run_cmd("git add -f uv.lock", cwd=root_path, check=False)
202
+ elif manager == "pnpm":
203
+ run_cmd("git add -f pnpm-lock.yaml", cwd=root_path, check=False)
151
204
 
152
205
  run_cmd("git add .", cwd=root_path)
153
206
 
@@ -0,0 +1,76 @@
1
+ """Release orchestration package.
2
+ This module provides a unified namespace for automated releases,
3
+ re-exporting specialized submodules while ensuring compatibility
4
+ with the existing test suite's mocks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from framework_m_studio.core.shell import run_cmd
10
+
11
+ from .cli import (
12
+ release_app,
13
+ release_auto_command,
14
+ release_mr_command,
15
+ release_plan_command,
16
+ release_prepare_command,
17
+ release_push_command,
18
+ release_schema_command,
19
+ release_tag_command,
20
+ )
21
+ from .gitops import (
22
+ GitLabProvider,
23
+ GitProviderProtocol,
24
+ _get_commits_since,
25
+ _get_last_tag,
26
+ push_release_branch,
27
+ )
28
+ from .models import Commit, ReleaseConfig, ReleasePackage, ReleaseSync, ReleaseUpdate
29
+ from .orchestrator import (
30
+ _determine_version_bump,
31
+ _load_release_config,
32
+ compute_release_plan,
33
+ create_or_update_mr,
34
+ prepare_release,
35
+ run_release_orchestration,
36
+ tag_and_release,
37
+ )
38
+ from .versioning import (
39
+ _bump_version,
40
+ _pin_inter_package_dependencies,
41
+ _prepend_changelog,
42
+ _update_file_version,
43
+ )
44
+
45
+ __all__ = [
46
+ "Commit",
47
+ "GitLabProvider",
48
+ "GitProviderProtocol",
49
+ "ReleaseConfig",
50
+ "ReleasePackage",
51
+ "ReleaseSync",
52
+ "ReleaseUpdate",
53
+ "_bump_version",
54
+ "_determine_version_bump",
55
+ "_get_commits_since",
56
+ "_get_last_tag",
57
+ "_load_release_config",
58
+ "_pin_inter_package_dependencies",
59
+ "_prepend_changelog",
60
+ "_update_file_version",
61
+ "compute_release_plan",
62
+ "create_or_update_mr",
63
+ "prepare_release",
64
+ "push_release_branch",
65
+ "release_app",
66
+ "release_auto_command",
67
+ "release_mr_command",
68
+ "release_plan_command",
69
+ "release_prepare_command",
70
+ "release_push_command",
71
+ "release_schema_command",
72
+ "release_tag_command",
73
+ "run_cmd",
74
+ "run_release_orchestration",
75
+ "tag_and_release",
76
+ ]
@@ -0,0 +1,201 @@
1
+ """CLI commands for release orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import cyclopts
11
+
12
+ from .gitops import GitLabProvider
13
+ from .models import ReleaseConfig
14
+ from .orchestrator import (
15
+ _load_release_config,
16
+ compute_release_plan,
17
+ create_or_update_mr,
18
+ prepare_release,
19
+ push_release_branch,
20
+ run_release_orchestration,
21
+ tag_and_release,
22
+ )
23
+
24
+ # Main CLI app instance
25
+ release_app = cyclopts.App(name="release")
26
+
27
+
28
+ @release_app.command(name="auto")
29
+ async def release_auto_command(
30
+ provider: str = "gitlab",
31
+ token: Annotated[str | None, cyclopts.Parameter(help="Git provider token")] = None,
32
+ project_id: Annotated[str | None, cyclopts.Parameter(help="Git project ID")] = None,
33
+ dry_run: Annotated[bool, cyclopts.Parameter(help="Dry run")] = False,
34
+ ) -> None:
35
+ """Automated multi-package release workflow."""
36
+ await run_release_orchestration(
37
+ provider=provider,
38
+ token=token
39
+ or os.environ.get("CITADEL_CI_TOKEN")
40
+ or os.environ.get("GITLAB_TOKEN")
41
+ or "",
42
+ project_id=project_id or os.environ.get("CI_PROJECT_ID") or "",
43
+ dry_run=dry_run,
44
+ )
45
+
46
+
47
+ @release_app.command(name="plan")
48
+ def release_plan_command(
49
+ config: str = "release-config.json", root: str | None = None, json: bool = False
50
+ ) -> None:
51
+ """Preview which components will be released."""
52
+ root_path = Path(root) if root else Path.cwd()
53
+ release_config = _load_release_config(config, root=root_path)
54
+ updates = compute_release_plan(release_config, root_path)
55
+ if json:
56
+ import json as _json
57
+
58
+ print(_json.dumps([u.model_dump() for u in updates], indent=2))
59
+ return
60
+ if not updates:
61
+ print("✅ No releases needed.")
62
+ return
63
+ for u in updates:
64
+ print(f"🚀 {u.component}: {u.current_version} -> {u.new_version} ({u.bump})")
65
+
66
+
67
+ @release_app.command(name="prepare")
68
+ def release_prepare_command(
69
+ config: str = "release-config.json",
70
+ branch: str = "release/next",
71
+ root: str | None = None,
72
+ no_sbom: bool = False,
73
+ dry_run: bool = False,
74
+ ) -> None:
75
+ """Prepare a release branch with bumped versions."""
76
+ root_path = Path(root) if root else Path.cwd()
77
+ try:
78
+ release_config = _load_release_config(config, root=root_path)
79
+ except FileNotFoundError:
80
+ if dry_run:
81
+ release_config = ReleaseConfig(packages=[])
82
+ else:
83
+ raise
84
+
85
+ updates = compute_release_plan(release_config, root_path)
86
+ if not updates:
87
+ if dry_run:
88
+ pass
89
+ else:
90
+ print("✅ No releases needed.")
91
+ return
92
+
93
+ if dry_run:
94
+ print(f"[DRY RUN] Would prepare branch {branch} for {len(updates)} components")
95
+ return
96
+ prepare_release(updates, branch, root_path, release_config, sbom=not no_sbom)
97
+ print(f"✅ Prepared release files for {branch}")
98
+
99
+
100
+ @release_app.command(name="push")
101
+ def release_push_command(
102
+ branch: str = "release/next",
103
+ host: str | None = None,
104
+ project_path: str | None = None,
105
+ token: str | None = None,
106
+ dry_run: bool = False,
107
+ ) -> None:
108
+ """Push the release branch to the origin remote."""
109
+ final_host = host or os.environ.get("CI_SERVER_HOST", "gitlab.com")
110
+ final_path = project_path or os.environ.get("CI_PROJECT_PATH", "")
111
+ final_token = (
112
+ token or os.environ.get("CITADEL_CI_TOKEN") or os.environ.get("GITLAB_TOKEN")
113
+ )
114
+ if dry_run:
115
+ print(f"[DRY RUN] Would push branch {branch} to {final_host}/{final_path}")
116
+ return
117
+ if not final_token:
118
+ print("Error: token missing", file=sys.stderr)
119
+ raise SystemExit(1)
120
+ push_release_branch(branch, final_host, final_path, final_token)
121
+ print(f"✅ Pushed branch {branch}")
122
+
123
+
124
+ @release_app.command(name="mr")
125
+ async def release_mr_command(
126
+ branch: str = "release/next",
127
+ target_branch: str = "main",
128
+ provider: str = "gitlab",
129
+ token: str | None = None,
130
+ project_id: str | None = None,
131
+ dry_run: bool = False,
132
+ ) -> None:
133
+ """Create or update a merge request."""
134
+ final_token = (
135
+ token or os.environ.get("CITADEL_CI_TOKEN") or os.environ.get("GITLAB_TOKEN")
136
+ )
137
+ final_project_id = project_id or os.environ.get("CI_PROJECT_ID")
138
+
139
+ if provider != "gitlab":
140
+ print(f"Provider '{provider}' not yet supported.", file=sys.stderr)
141
+ raise SystemExit(1)
142
+
143
+ if dry_run:
144
+ print(f"[DRY RUN] Would create/update MR for {branch} into {target_branch}")
145
+ return
146
+ if not final_token or not final_project_id:
147
+ print("Error: token/project_id missing", file=sys.stderr)
148
+ raise SystemExit(1)
149
+ provider_cls = GitLabProvider(token=final_token, project_id=final_project_id)
150
+ title = "chore(release): automated multi-package release"
151
+ desc = "## Automated Release"
152
+ await create_or_update_mr(branch, target_branch, title, desc, provider_cls)
153
+
154
+
155
+ @release_app.command(name="tag")
156
+ async def release_tag_command(
157
+ config: str = "release-config.json",
158
+ provider: str = "gitlab",
159
+ token: str | None = None,
160
+ project_id: str | None = None,
161
+ dry_run: bool = False,
162
+ ) -> None:
163
+ """Create tags and releases for updated components."""
164
+ final_token = (
165
+ token or os.environ.get("CITADEL_CI_TOKEN") or os.environ.get("GITLAB_TOKEN")
166
+ )
167
+ final_project_id = project_id or os.environ.get("CI_PROJECT_ID")
168
+ root_path = Path.cwd()
169
+
170
+ try:
171
+ release_config = _load_release_config(config, root=root_path)
172
+ except Exception:
173
+ if provider != "gitlab":
174
+ print(f"Provider '{provider}' not yet supported.", file=sys.stderr)
175
+ raise SystemExit(1) from None
176
+ raise
177
+
178
+ git_provider: GitLabProvider | None = None
179
+ if final_token and final_project_id:
180
+ if provider == "gitlab":
181
+ git_provider = GitLabProvider(
182
+ token=final_token, project_id=final_project_id
183
+ )
184
+ else:
185
+ print(f"Provider '{provider}' not yet supported.", file=sys.stderr)
186
+ raise SystemExit(1)
187
+ await tag_and_release(release_config, root_path, git_provider, dry_run)
188
+
189
+
190
+ def release_schema_command(output: str | None = None) -> None:
191
+ """Generate JSON schema for the release configuration."""
192
+ from .models import ReleaseConfig
193
+
194
+ schema = ReleaseConfig.model_json_schema()
195
+ import json as _json
196
+
197
+ content = _json.dumps(schema, indent=2)
198
+ if output:
199
+ Path(output).write_text(content)
200
+ else:
201
+ print(content)
@@ -0,0 +1,194 @@
1
+ import os
2
+ import re
3
+ import subprocess
4
+ import sys
5
+ from typing import Any, Protocol, runtime_checkable
6
+
7
+ import httpx
8
+
9
+ from framework_m_studio.core.shell import run_cmd
10
+
11
+ from .models import Commit
12
+
13
+
14
+ @runtime_checkable
15
+ class GitProviderProtocol(Protocol):
16
+ """Protocol for Git platform providers (GitLab, GitHub, etc.)."""
17
+
18
+ async def find_mr(self, branch: str) -> dict[str, Any] | None: ...
19
+
20
+ async def create_mr(
21
+ self, title: str, desc: str, branch: str, target_branch: str
22
+ ) -> dict[str, Any]: ...
23
+
24
+ async def update_mr(self, iid: str, title: str, desc: str) -> dict[str, Any]: ...
25
+
26
+ async def create_release(self, tag: str, changelog: str) -> dict[str, Any]: ...
27
+
28
+
29
+ class GitLabProvider:
30
+ """GitLab-specific implementation of the GitProviderProtocol."""
31
+
32
+ def __init__(self, token: str, project_id: str):
33
+ self.token = token
34
+ self.project_id = project_id
35
+ self.api_url = os.environ.get("CI_API_V4_URL", "https://gitlab.com/api/v4")
36
+
37
+ async def _request(
38
+ self, method: str, path: str, data: dict[str, Any] | None = None
39
+ ) -> dict[str, Any] | None:
40
+ """Make an authenticated request to the GitLab API using httpx."""
41
+ url = f"{self.api_url}/projects/{self.project_id}{path}"
42
+ headers = {
43
+ "PRIVATE-TOKEN": self.token,
44
+ "Content-Type": "application/json",
45
+ }
46
+
47
+ try:
48
+ async with httpx.AsyncClient(timeout=60.0) as client:
49
+ response = await client.request(
50
+ method, url, headers=headers, json=data if data else None
51
+ )
52
+ response.raise_for_status()
53
+ return response.json() if response.content.strip() else {}
54
+ except httpx.HTTPStatusError as e:
55
+ print(
56
+ f"Error: GitLab API {method} {path} failed with {e.response.status_code}",
57
+ file=sys.stderr,
58
+ )
59
+ return None
60
+ except Exception as e:
61
+ print(f"Error: GitLab API request failed: {e}", file=sys.stderr)
62
+ return None
63
+
64
+ async def find_mr(self, branch: str) -> dict[str, Any] | None:
65
+ path = f"/merge_requests?source_branch={branch}&state=opened"
66
+ mrs = await self._request("GET", path)
67
+ return mrs[0] if isinstance(mrs, list) and mrs else None
68
+
69
+ async def create_mr(
70
+ self, title: str, desc: str, branch: str, target_branch: str
71
+ ) -> dict[str, Any]:
72
+ path = "/merge_requests"
73
+ data = {
74
+ "title": title,
75
+ "description": desc,
76
+ "source_branch": branch,
77
+ "target_branch": target_branch,
78
+ "remove_source_branch": True,
79
+ }
80
+ res = await self._request("POST", path, data)
81
+ return res if res is not None else {}
82
+
83
+ async def update_mr(self, iid: str, title: str, desc: str) -> dict[str, Any]:
84
+ path = f"/merge_requests/{iid}"
85
+ data = {"title": title, "description": desc}
86
+ res = await self._request("PUT", path, data)
87
+ return res if res is not None else {}
88
+
89
+ async def create_release(self, tag: str, changelog: str) -> dict[str, Any]:
90
+ path = "/releases"
91
+ data = {"tag_name": tag, "description": changelog}
92
+ res = await self._request("POST", path, data)
93
+ return res if res is not None else {}
94
+
95
+
96
+ def _get_last_tag(component: str) -> str | None:
97
+ """Get the last git tag for a component (e.g., pkg-name@v1.2.3)."""
98
+ match_pattern = f"{component}@v*"
99
+ try:
100
+ # Use list-based command for security
101
+ tag = run_cmd(
102
+ ["git", "describe", "--tags", "--abbrev=0", f"--match={match_pattern}"],
103
+ check=False,
104
+ )
105
+ return tag.strip() if tag else None
106
+ except Exception:
107
+ return None
108
+
109
+
110
+ def _get_commits_since(tag: str | None) -> list[Commit]:
111
+ """Get list of conventional commits since a tag (or all if tag is None)."""
112
+ range_spec = f"{tag}..HEAD" if tag else "HEAD"
113
+ sep = "||COMMIT_SEP||"
114
+ log_format = f"%H%n%s%n%b%n{sep}"
115
+ # Use list-based command for security
116
+ output = run_cmd(["git", "log", range_spec, f"--pretty=format:{log_format}"])
117
+
118
+ commits = []
119
+ if output:
120
+ raw_commits = output.split(sep)
121
+ for raw in raw_commits:
122
+ if not raw.strip():
123
+ continue
124
+ lines = raw.strip().split("\n")
125
+ if len(lines) < 2:
126
+ continue
127
+ parsed = _parse_commit_message("\n".join(lines[1:]), lines[0])
128
+ if parsed:
129
+ commits.append(parsed)
130
+ return commits
131
+
132
+
133
+ def _parse_commit_message(full_message: str, hash_str: str = "") -> Commit | None:
134
+ """Parse a conventional commit message into a Commit model."""
135
+ lines = full_message.strip().split("\n")
136
+ subject = lines[0]
137
+ body = "\n".join(lines[1:]).strip() if len(lines) > 1 else ""
138
+ pattern = r"^(\w+)(?:\(([\w\-\.\*]+)\))?(!)?:\s*(.+)"
139
+ match = re.match(pattern, subject)
140
+
141
+ if not match:
142
+ return None
143
+
144
+ commit_type, scope, breaking_mark, description = match.groups()
145
+ is_breaking = bool(breaking_mark) or "BREAKING CHANGE:" in body
146
+
147
+ body_tags = []
148
+ for line in body.split("\n"):
149
+ tag_match = re.search(r"^(?:Releases|Packages):\s*(.+)", line, re.I)
150
+ if tag_match:
151
+ body_tags.extend(
152
+ [
153
+ s.strip()
154
+ for s in tag_match.group(1).replace(",", " ").split()
155
+ if s.strip()
156
+ ]
157
+ )
158
+
159
+ return Commit(
160
+ hash=hash_str,
161
+ subject=subject,
162
+ body=body,
163
+ type=commit_type,
164
+ scope=scope,
165
+ breaking=is_breaking,
166
+ description=description,
167
+ body_tags=body_tags,
168
+ )
169
+
170
+
171
+ def push_release_branch(branch: str, host: str, project_path: str, token: str) -> None:
172
+ """Push the local release branch directly to the remote (using token)."""
173
+ normalized_path = project_path.lstrip("/")
174
+ if not normalized_path:
175
+ raise ValueError("Git project path is empty. Cannot determine remote URL.")
176
+ # Inject token into the remote URL for direct push
177
+ remote_url = f"https://oauth2:{token}@{host}/{normalized_path}.git"
178
+ # Use list-based command for security (Bandit B605)
179
+ # Use --force to ensure we can update the transient release branch in CI/CD
180
+ cmd = ["git", "push", "--force", remote_url, f"{branch}:refs/heads/{branch}"]
181
+ try:
182
+ # We use subprocess.run directly for fine-grained masking of the token in logs
183
+ result = subprocess.run(cmd, capture_output=True, text=True, check=False)
184
+ if result.returncode != 0:
185
+ error_msg = result.stderr or result.stdout or "Unknown git error"
186
+ # Mask the token in any error output
187
+ cleaned_error = error_msg.replace(token, "[MASKED]")
188
+ print(f"Error pushing release branch: {cleaned_error}", file=sys.stderr)
189
+ raise SystemExit(1)
190
+ except Exception as e:
191
+ # Mask the token in any exception message
192
+ cleaned_error = str(e).replace(token, "[MASKED]")
193
+ print(f"Failed to execute git push: {cleaned_error}", file=sys.stderr)
194
+ raise SystemExit(1) from None