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.
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/CHANGELOG.md +35 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/PKG-INFO +3 -3
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/pyproject.toml +11 -4
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/deps.py +69 -16
- framework_m_studio-0.8.4/src/framework_m_studio/cli/release/__init__.py +76 -0
- framework_m_studio-0.8.4/src/framework_m_studio/cli/release/cli.py +201 -0
- framework_m_studio-0.8.4/src/framework_m_studio/cli/release/gitops.py +194 -0
- framework_m_studio-0.8.4/src/framework_m_studio/cli/release/models.py +76 -0
- framework_m_studio-0.8.4/src/framework_m_studio/cli/release/orchestrator.py +389 -0
- framework_m_studio-0.8.4/src/framework_m_studio/cli/release/versioning.py +172 -0
- 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
- 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
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/static/index.html +2 -2
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/package.json +1 -1
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/package.json.template +4 -4
- framework_m_studio-0.8.4/studio_ui/CHANGELOG.md +37 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/package.json +13 -13
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_release_cli.py +271 -74
- framework_m_studio-0.8.2/src/framework_m_studio/cli/release.py +0 -888
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/.gitignore +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/LICENSE +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/README.md +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/docs/api/index.md +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/docs/user-guide.md +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/__init__.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/app.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/__init__.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/codegen.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/constants.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/contrib.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/dev.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/docs.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/new.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/quality.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/studio.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/utility.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/cli/verify.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/__init__.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/parser.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/templates/doctype.py.jinja2 +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/templates/test_doctype.py.jinja2 +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/test_generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/codegen/transformer.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/core/frontend.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/core/gitops.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/core/shell.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/discovery.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/ast_parsers.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/audit.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/exporters.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/formatters.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/docs/openapi.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/__init__.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/adapter.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/github_provider.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/git/protocol.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/protocol_scanner.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/py.typed +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/routes.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/sdk_generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/static/.gitkeep +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/static/favicon.ico +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/.gitignore +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/.npmrc +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/README.md +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/index.html +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/App.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/components/AppNavbar.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/components/AppSidebar.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/index.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/pages/LoginPage.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/src/theme.css +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/tsconfig.json +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/frontend/vite.config.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/Dashboard.tsx.template +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/README.md.template +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/plugin.config.ts.template +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/templates/plugin/tsconfig.json.template +0 -0
- {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
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/src/framework_m_studio/workspace.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/.gitignore +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/.npmrc +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/Dockerfile +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/README.md +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/eslint.config.js +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/index.html +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/pnpm-lock.yaml +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/App.css +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/App.test.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/App.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/AutoForm.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/AutoTable.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/CodePreview.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/ControllerEditor.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/ERDView.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/FieldEditor.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/LayoutDesigner.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/ModuleExplorer.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/NestedEditorDrawer.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/SandboxPreview.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/TableFieldEditor.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/breadcrumb/index.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/fields/DefaultFieldComponents.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/fields/index.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/fields/resolveFieldComponent.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/layout/index.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/components/menu/index.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/index.css +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/index.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/doctypes/edit.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/doctypes/index.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/doctypes/list.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/pages/index.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/ThemeContext.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/constants.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/data.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/theme.tsx +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/providers/useTheme.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/registry/defaultComponents.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/registry/fieldComponents.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/registry/index.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/services/mock_data.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/setupTests.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/utils/mockDataGenerator.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/src/vite-env.d.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/tsconfig.json +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/tsconfig.node.json +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/vite.config.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/studio_ui/vitest.config.ts +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/cli/test_new.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/cli/test_verify.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/conftest.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/core/test_frontend.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/core/test_gitops.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/core/test_shell.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_api_routes.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_cli.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_cli_openapi.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_contrib_cli.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_deps_cli.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_dev.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_dev_cli_coverage.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_discovery.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_docs_audit.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_docs_generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_docs_generator_enhanced.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_frontend_integration.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_git_adapter.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_git_protocol.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_github_provider.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_new_cli.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_parser.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_protocol_scanner.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_quality_cli.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_screenshot_pipeline.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_sdk_generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_studio.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_studio_codegen_generator.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_transformer.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_utility_cli.py +0 -0
- {framework_m_studio-0.8.2 → framework_m_studio-0.8.4}/tests/test_utility_cli_enhanced.py +0 -0
- {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.
|
|
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.
|
|
24
|
-
Requires-Dist: framework-m>=0.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.
|
|
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.
|
|
25
|
-
"framework-m>=0.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
|
|
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:
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
if
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|