mainsequence 6.0.36__tar.gz → 6.0.38__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.
- {mainsequence-6.0.36 → mainsequence-6.0.38}/PKG-INFO +1 -1
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/maintenance/project-maintenance/SKILL.md +20 -7
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/api.py +2 -2
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/cli.py +163 -43
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/local_ops.py +39 -0
- mainsequence-6.0.38/mainsequence/cli/ssh_utils.py +365 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/models_foundry.py +41 -14
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence.egg-info/SOURCES.txt +2 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/pyproject.toml +1 -1
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_cli.py +347 -13
- mainsequence-6.0.38/tests/test_local_ops.py +59 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_pod_project_resolution.py +3 -3
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_project_branch_contract.py +63 -3
- mainsequence-6.0.38/tests/test_ssh_utils.py +187 -0
- mainsequence-6.0.36/mainsequence/cli/ssh_utils.py +0 -184
- {mainsequence-6.0.36 → mainsequence-6.0.38}/LICENSE +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/README.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/agent_scaffold/skills/sdk_project_execution/SKILL.md +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/__main__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/bootstrap.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/config.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/migrations.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/project_status.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/cli/ui.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/base.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/client.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/dtype_codec.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/metatables/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/metatables/core.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/models_user.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/client/utils.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/defaults.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/logconf.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/__main__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/models.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/future_registry.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/hashing.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/alembic.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/env.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/provider.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/registry.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/schema_names.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/project_skills.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/runtime_context.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/setup.cfg +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_auth_precedence.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_build_operations_hashing.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_cli_migrations.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_client.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_data_access_mixin_dimension_audit.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_data_node_storage_dimension_queries.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_data_node_update_flow.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_dependency_extras.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_duckdb_interface_dimensions.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_filter_normalization.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_instrumentation.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_logconf.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_meta_table_migrations.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_meta_tables_client_models.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_project_batch_jobs_from_file.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_project_skills.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_run_configuration.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_schema_names.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_secret_client_model.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_source_table_configuration.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_sqlite_interface_dimensions.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_update_runner_uid_runtime.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_update_statistics.py +0 -0
- {mainsequence-6.0.36 → mainsequence-6.0.38}/tests/test_update_uid_guards.py +0 -0
|
@@ -192,6 +192,12 @@ a detached checkout and rejects a Git branch that is not registered under the
|
|
|
192
192
|
logical Project. Do not bypass that validation or supply a ProjectBranch UID
|
|
193
193
|
manually. Register or select the correct Git branch first.
|
|
194
194
|
|
|
195
|
+
After that branch preflight, `--dry-run` resolves the existing `uv` executable,
|
|
196
|
+
previews its patch version without mutation, requests the backend-owned tag for
|
|
197
|
+
that future version, rejects an invalid or existing local tag, prints the
|
|
198
|
+
complete plan, and returns without generating an SSH key, querying private
|
|
199
|
+
remote refs, changing dependencies or project files, or mutating Git state.
|
|
200
|
+
|
|
195
201
|
After review:
|
|
196
202
|
|
|
197
203
|
```bash
|
|
@@ -200,13 +206,20 @@ mainsequence project sync --path . -m "<specific commit message>"
|
|
|
200
206
|
|
|
201
207
|
The canonical command always:
|
|
202
208
|
|
|
203
|
-
1.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
1. selects `mainsequence-<repository-slug>-<first-16-sha256>` from the normalized
|
|
210
|
+
`host[:non-default-port]/repository/path`, never from the repository basename alone and never
|
|
211
|
+
from a legacy basename-only key;
|
|
212
|
+
2. previews the `uv` patch version, requests the backend-owned ProjectBranch tag, and rejects an
|
|
213
|
+
invalid or existing local tag;
|
|
214
|
+
3. registers a new or inaccessible key through the owning Project and verifies a dry-run push with
|
|
215
|
+
that forced identity;
|
|
216
|
+
4. queries the exact tag ref on `origin` and stops if it exists or cannot be checked;
|
|
217
|
+
5. applies the patch version bump and verifies it matches the preview;
|
|
218
|
+
6. runs `uv lock` and `uv sync`;
|
|
219
|
+
7. exports locked production requirements;
|
|
220
|
+
8. stages and commits the changes;
|
|
221
|
+
9. creates the returned annotated tag unchanged; and
|
|
222
|
+
10. atomically pushes the explicit branch and tag refs with `--follow-tags`.
|
|
210
223
|
|
|
211
224
|
Do not offer alternate bump modes, a no-push mode, or a hand-written sequence
|
|
212
225
|
of equivalent commands. Do not call `sync-after-commit` or any backend repair
|
|
@@ -5116,7 +5116,7 @@ def bulk_delete_projects(
|
|
|
5116
5116
|
return data
|
|
5117
5117
|
|
|
5118
5118
|
|
|
5119
|
-
def add_deploy_key(
|
|
5119
|
+
def add_deploy_key(project_uid: str, key_title: str, public_key: str) -> None:
|
|
5120
5120
|
"""
|
|
5121
5121
|
Add a deploy key for the project.
|
|
5122
5122
|
|
|
@@ -5125,7 +5125,7 @@ def add_deploy_key(project_branch_uid: str, key_title: str, public_key: str) ->
|
|
|
5125
5125
|
"""
|
|
5126
5126
|
r = authed(
|
|
5127
5127
|
"POST",
|
|
5128
|
-
f"/api/v1/
|
|
5128
|
+
f"/api/v1/projects/{resolve_project_uid(project_uid)}/add-deploy-key/",
|
|
5129
5129
|
{"key_title": key_title, "public_key": public_key},
|
|
5130
5130
|
)
|
|
5131
5131
|
r.raise_for_status()
|
|
@@ -214,6 +214,7 @@ from .local_ops import (
|
|
|
214
214
|
run_cmd,
|
|
215
215
|
run_uv,
|
|
216
216
|
uv_export_requirements,
|
|
217
|
+
uv_preview_patch_version,
|
|
217
218
|
uv_project_version,
|
|
218
219
|
)
|
|
219
220
|
from .migrations import migrations as migrations_group
|
|
@@ -228,9 +229,16 @@ from .pydantic_cli import (
|
|
|
228
229
|
from .sdk_utils import fetch_latest_sdk_version, normalize_version, read_local_sdk_version
|
|
229
230
|
from .ssh_utils import (
|
|
230
231
|
ensure_key_for_repo,
|
|
232
|
+
git_ssh_environment,
|
|
231
233
|
open_folder,
|
|
232
234
|
open_signed_terminal,
|
|
235
|
+
repository_ssh_key_paths,
|
|
236
|
+
require_ssh_git_origin,
|
|
233
237
|
start_agent_and_add_key,
|
|
238
|
+
verify_git_push_access,
|
|
239
|
+
verify_git_remote_access,
|
|
240
|
+
verify_git_remote_tag_absent,
|
|
241
|
+
verify_git_tag_absent,
|
|
234
242
|
)
|
|
235
243
|
from .ui import error, info, print_kv, print_table, status, success, warn
|
|
236
244
|
|
|
@@ -674,6 +682,48 @@ def _resolve_project_repository_ssh_url(project: dict) -> str:
|
|
|
674
682
|
return repository_ssh_url
|
|
675
683
|
|
|
676
684
|
|
|
685
|
+
def _ensure_project_repository_ssh_access(
|
|
686
|
+
*,
|
|
687
|
+
origin: str,
|
|
688
|
+
project_ref: str | None,
|
|
689
|
+
verify_access,
|
|
690
|
+
) -> tuple[pathlib.Path, str, dict[str, str]]:
|
|
691
|
+
expected_key_path, expected_public_key_path = repository_ssh_key_paths(origin)
|
|
692
|
+
keypair_existed = expected_key_path.is_file() and expected_public_key_path.is_file()
|
|
693
|
+
key_path, _public_key_path, public_key = ensure_key_for_repo(origin)
|
|
694
|
+
env = git_ssh_environment(key_path)
|
|
695
|
+
|
|
696
|
+
def register_key() -> None:
|
|
697
|
+
normalized_project_ref = str(project_ref or "").strip()
|
|
698
|
+
if not normalized_project_ref:
|
|
699
|
+
raise ApiError(
|
|
700
|
+
"The repository SSH key is not authorized and no MAIN_SEQUENCE_PROJECT_UID "
|
|
701
|
+
"is available for deploy-key registration."
|
|
702
|
+
)
|
|
703
|
+
key_title = str(platform.node() or "").strip()
|
|
704
|
+
if not key_title or "\n" in key_title or "\r" in key_title:
|
|
705
|
+
raise ApiError("Local hostname must contain one non-empty line.")
|
|
706
|
+
try:
|
|
707
|
+
add_deploy_key(normalized_project_ref, key_title, public_key)
|
|
708
|
+
except Exception as exc:
|
|
709
|
+
raise ApiError(f"Project deploy-key registration failed: {exc}") from exc
|
|
710
|
+
|
|
711
|
+
if not keypair_existed:
|
|
712
|
+
register_key()
|
|
713
|
+
else:
|
|
714
|
+
try:
|
|
715
|
+
verify_access(env)
|
|
716
|
+
return key_path, public_key, env
|
|
717
|
+
except RuntimeError:
|
|
718
|
+
register_key()
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
verify_access(env)
|
|
722
|
+
except RuntimeError as exc:
|
|
723
|
+
raise ApiError(str(exc)) from exc
|
|
724
|
+
return key_path, public_key, env
|
|
725
|
+
|
|
726
|
+
|
|
677
727
|
def _project_identity_value(project: dict) -> str:
|
|
678
728
|
"""Return the logical Project public UID."""
|
|
679
729
|
return str(project.get("uid") or "").strip()
|
|
@@ -7461,7 +7511,7 @@ def project_create_cmd(
|
|
|
7461
7511
|
project_name:
|
|
7462
7512
|
Project name. If omitted, prompt is shown.
|
|
7463
7513
|
project_type:
|
|
7464
|
-
|
|
7514
|
+
Immutable type of the logical Project, shared by every ProjectBranch.
|
|
7465
7515
|
default_metatables_data_source_uid:
|
|
7466
7516
|
Required logical Project default MetaTables data source UID. It is
|
|
7467
7517
|
assigned automatically to the initial main ProjectBranch.
|
|
@@ -9786,7 +9836,7 @@ def project_set_up_locally(
|
|
|
9786
9836
|
Clone a project locally and provision runtime `.env`.
|
|
9787
9837
|
|
|
9788
9838
|
Workflow:
|
|
9789
|
-
- ensure
|
|
9839
|
+
- ensure, register when needed, and verify a repository-specific SSH key,
|
|
9790
9840
|
- clone repository into local projects root,
|
|
9791
9841
|
- build local runtime auth/backend entries for the active auth mode,
|
|
9792
9842
|
- write/update `.env` with local runtime values.
|
|
@@ -9832,7 +9882,6 @@ def project_set_up_locally(
|
|
|
9832
9882
|
error(str(e))
|
|
9833
9883
|
raise typer.Exit(1) from e
|
|
9834
9884
|
|
|
9835
|
-
project_branch_uid = str(project_branch.get("uid") or "").strip()
|
|
9836
9885
|
repository_branch = str(project_branch.get("repository_branch") or "").strip()
|
|
9837
9886
|
is_initialized = project_branch.get("is_initialized")
|
|
9838
9887
|
|
|
@@ -9854,24 +9903,24 @@ def project_set_up_locally(
|
|
|
9854
9903
|
target_dir = projects_root / f"{name}-{project_uid}"
|
|
9855
9904
|
projects_root.mkdir(parents=True, exist_ok=True)
|
|
9856
9905
|
|
|
9857
|
-
|
|
9858
|
-
|
|
9906
|
+
if target_dir.exists():
|
|
9907
|
+
warn(f"Target already exists: {target_dir}")
|
|
9908
|
+
raise typer.Exit(2)
|
|
9859
9909
|
|
|
9860
|
-
# Best-effort deploy key (do not fail set-up-locally on this)
|
|
9861
9910
|
try:
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9911
|
+
key_path, pub, _git_env = _ensure_project_repository_ssh_access(
|
|
9912
|
+
origin=repo,
|
|
9913
|
+
project_ref=project_uid,
|
|
9914
|
+
verify_access=lambda env: verify_git_remote_access(repo, env),
|
|
9915
|
+
)
|
|
9916
|
+
except (ApiError, RuntimeError, ValueError) as exc:
|
|
9917
|
+
error(f"Repository SSH key setup failed: {exc}")
|
|
9918
|
+
raise typer.Exit(1) from exc
|
|
9866
9919
|
|
|
9920
|
+
copied = _copy_clipboard(pub)
|
|
9867
9921
|
agent_env = start_agent_and_add_key(key_path)
|
|
9868
9922
|
|
|
9869
|
-
|
|
9870
|
-
warn(f"Target already exists: {target_dir}")
|
|
9871
|
-
raise typer.Exit(2)
|
|
9872
|
-
|
|
9873
|
-
env = os.environ.copy() | agent_env
|
|
9874
|
-
env["GIT_SSH_COMMAND"] = f'ssh -i "{str(key_path)}" -o IdentitiesOnly=yes'
|
|
9923
|
+
env = git_ssh_environment(key_path, base_env=os.environ.copy() | agent_env)
|
|
9875
9924
|
|
|
9876
9925
|
with status(f"Cloning repo into {target_dir}..."):
|
|
9877
9926
|
rc = subprocess.call(
|
|
@@ -10034,7 +10083,16 @@ def project_open_signed_terminal(
|
|
|
10034
10083
|
|
|
10035
10084
|
origin = git_origin(project_dir)
|
|
10036
10085
|
name = repo_name_from_git_url(origin) or project_dir.name
|
|
10037
|
-
|
|
10086
|
+
project_ref = str(project_id or _read_project_ref_from_env_file(project_dir) or "").strip()
|
|
10087
|
+
try:
|
|
10088
|
+
key_path, _public_key, _git_env = _ensure_project_repository_ssh_access(
|
|
10089
|
+
origin=origin,
|
|
10090
|
+
project_ref=project_ref or None,
|
|
10091
|
+
verify_access=lambda env: verify_git_remote_access(origin, env),
|
|
10092
|
+
)
|
|
10093
|
+
except (ApiError, RuntimeError, ValueError) as exc:
|
|
10094
|
+
error(f"Repository SSH key setup failed: {exc}")
|
|
10095
|
+
raise typer.Exit(1) from exc
|
|
10038
10096
|
open_signed_terminal(str(project_dir), key_path, name)
|
|
10039
10097
|
|
|
10040
10098
|
|
|
@@ -10301,18 +10359,23 @@ def project_sync(
|
|
|
10301
10359
|
project_id: str | None = typer.Argument(None, help="Project UID"),
|
|
10302
10360
|
path: str | None = typer.Option(None, "--path", help="Project directory"),
|
|
10303
10361
|
message_opt: str | None = typer.Option(None, "--message", "-m", help="Git commit message"),
|
|
10304
|
-
dry_run: bool = typer.Option(
|
|
10362
|
+
dry_run: bool = typer.Option(
|
|
10363
|
+
False,
|
|
10364
|
+
"--dry-run",
|
|
10365
|
+
help="Run read-only preflight and print steps without generating keys or changing the project",
|
|
10366
|
+
),
|
|
10305
10367
|
):
|
|
10306
10368
|
"""
|
|
10307
10369
|
Run end-to-end sync workflow for project dependencies and git state.
|
|
10308
10370
|
|
|
10309
10371
|
Workflow:
|
|
10310
|
-
1.
|
|
10311
|
-
2.
|
|
10312
|
-
3.
|
|
10313
|
-
4.
|
|
10314
|
-
5.
|
|
10315
|
-
6.
|
|
10372
|
+
1. preview the patch version and backend-owned ProjectBranch tag,
|
|
10373
|
+
2. reject local and remote collisions before mutating the project,
|
|
10374
|
+
3. apply and verify the patch version via `uv version`,
|
|
10375
|
+
4. run `uv lock` + `uv sync`,
|
|
10376
|
+
5. export locked `requirements.txt`,
|
|
10377
|
+
6. commit the changes and create that annotated tag,
|
|
10378
|
+
7. atomically push the branch and tag.
|
|
10316
10379
|
|
|
10317
10380
|
Parameters
|
|
10318
10381
|
----------
|
|
@@ -10323,7 +10386,8 @@ def project_sync(
|
|
|
10323
10386
|
path:
|
|
10324
10387
|
Explicit local path.
|
|
10325
10388
|
dry_run:
|
|
10326
|
-
|
|
10389
|
+
Run read-only preflight and print the plan without generating keys or
|
|
10390
|
+
changing project files, dependencies, Git state, or backend state.
|
|
10327
10391
|
|
|
10328
10392
|
Examples
|
|
10329
10393
|
--------
|
|
@@ -10369,46 +10433,90 @@ def project_sync(
|
|
|
10369
10433
|
f"Git branch {git_branch!r} to ProjectBranch {project_branch_uid}."
|
|
10370
10434
|
)
|
|
10371
10435
|
|
|
10372
|
-
ensure_venv(project_dir)
|
|
10373
|
-
|
|
10374
10436
|
origin = git_origin(project_dir)
|
|
10375
10437
|
repo_name = repo_name_from_git_url(origin) or project_dir.name
|
|
10376
|
-
|
|
10438
|
+
try:
|
|
10439
|
+
require_ssh_git_origin(origin)
|
|
10440
|
+
except ValueError as exc:
|
|
10441
|
+
error(f"Project sync preflight failed: {exc}")
|
|
10442
|
+
raise typer.Exit(1) from exc
|
|
10443
|
+
|
|
10444
|
+
try:
|
|
10445
|
+
ensure_venv(project_dir)
|
|
10446
|
+
uv = ensure_uv_installed(project_dir)
|
|
10447
|
+
current_version = uv_project_version(uv, cwd=project_dir)
|
|
10448
|
+
next_version = uv_preview_patch_version(uv, cwd=project_dir)
|
|
10449
|
+
tag = render_project_branch_default_redeployment_tag(
|
|
10450
|
+
project_branch_uid,
|
|
10451
|
+
version=next_version,
|
|
10452
|
+
)
|
|
10453
|
+
verify_git_tag_absent(project_dir, tag)
|
|
10454
|
+
except (ApiError, RuntimeError) as exc:
|
|
10455
|
+
error(f"Project sync tag preflight failed: {exc}")
|
|
10456
|
+
raise typer.Exit(1) from exc
|
|
10377
10457
|
|
|
10378
10458
|
steps = [
|
|
10379
|
-
"
|
|
10380
|
-
"uv version --bump patch",
|
|
10459
|
+
"preview uv patch version",
|
|
10381
10460
|
"request backend default redeployment tag",
|
|
10461
|
+
"verify backend tag does not exist locally",
|
|
10462
|
+
"ensure repository SSH key",
|
|
10463
|
+
"register a new or inaccessible SSH key through the owning Project",
|
|
10464
|
+
"git push --dry-run --follow-tags origin HEAD:refs/heads/<branch>",
|
|
10465
|
+
"verify exact backend tag does not exist remotely",
|
|
10466
|
+
"uv version --bump patch",
|
|
10467
|
+
"verify bumped version matches the preflight version",
|
|
10382
10468
|
"uv lock",
|
|
10383
10469
|
"uv sync",
|
|
10384
10470
|
"uv export (locked) -> requirements.txt",
|
|
10385
10471
|
"git add -A",
|
|
10386
10472
|
f'git commit -m "{safe_message}"',
|
|
10387
10473
|
"git tag -a <backend tag> -m <backend tag>",
|
|
10388
|
-
"git push --follow-tags",
|
|
10474
|
+
"git push --atomic --follow-tags origin HEAD:refs/heads/<branch> refs/tags/<backend tag>:refs/tags/<backend tag>",
|
|
10389
10475
|
]
|
|
10390
10476
|
|
|
10477
|
+
print_kv(
|
|
10478
|
+
"Sync release",
|
|
10479
|
+
[
|
|
10480
|
+
("Current version", current_version),
|
|
10481
|
+
("Next version", next_version),
|
|
10482
|
+
("Branch tag", tag),
|
|
10483
|
+
],
|
|
10484
|
+
)
|
|
10391
10485
|
print_table("Sync plan", ["Step"], [[s] for s in steps])
|
|
10392
10486
|
|
|
10393
10487
|
if dry_run:
|
|
10394
|
-
warn("Dry run: no
|
|
10488
|
+
warn("Dry run: read-only preflight complete; no changes made.")
|
|
10395
10489
|
return
|
|
10396
10490
|
|
|
10397
|
-
|
|
10398
|
-
|
|
10491
|
+
try:
|
|
10492
|
+
_key_path, _public_key, env = _ensure_project_repository_ssh_access(
|
|
10493
|
+
origin=origin,
|
|
10494
|
+
project_ref=project_ref,
|
|
10495
|
+
verify_access=lambda ssh_env: verify_git_push_access(
|
|
10496
|
+
project_dir,
|
|
10497
|
+
git_branch,
|
|
10498
|
+
ssh_env,
|
|
10499
|
+
),
|
|
10500
|
+
)
|
|
10501
|
+
except (ApiError, RuntimeError, ValueError) as exc:
|
|
10502
|
+
error(f"Project sync SSH preflight failed: {exc}")
|
|
10503
|
+
raise typer.Exit(1) from exc
|
|
10504
|
+
|
|
10505
|
+
try:
|
|
10506
|
+
verify_git_remote_tag_absent(project_dir, tag, env)
|
|
10507
|
+
except RuntimeError as exc:
|
|
10508
|
+
error(f"Project sync remote tag preflight failed: {exc}")
|
|
10509
|
+
raise typer.Exit(1) from exc
|
|
10399
10510
|
|
|
10400
|
-
uv = ensure_uv_installed(project_dir)
|
|
10401
10511
|
with status("Running uv + git sync steps..."):
|
|
10402
10512
|
run_uv(uv, ["version", "--bump", "patch"], cwd=project_dir, env=env)
|
|
10403
10513
|
version = uv_project_version(uv, cwd=project_dir, env=env)
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10514
|
+
if version != next_version:
|
|
10515
|
+
error(
|
|
10516
|
+
f"Project sync version verification failed: uv produced {version}; "
|
|
10517
|
+
f"preflight expected {next_version}."
|
|
10408
10518
|
)
|
|
10409
|
-
|
|
10410
|
-
error(f"Project sync tag resolution failed: {exc}")
|
|
10411
|
-
raise typer.Exit(1) from exc
|
|
10519
|
+
raise typer.Exit(1)
|
|
10412
10520
|
run_uv(uv, ["lock"], cwd=project_dir, env=env)
|
|
10413
10521
|
run_uv(uv, ["sync"], cwd=project_dir, env=env)
|
|
10414
10522
|
# `uv sync` can prune ad hoc packages from `.venv`, including a `uv`
|
|
@@ -10426,7 +10534,19 @@ def project_sync(
|
|
|
10426
10534
|
run_cmd(["git", "add", "-A"], cwd=project_dir, env=env)
|
|
10427
10535
|
run_cmd(["git", "commit", "-m", safe_message], cwd=project_dir, env=env)
|
|
10428
10536
|
run_cmd(["git", "tag", "-a", tag, "-m", tag], cwd=project_dir, env=env)
|
|
10429
|
-
run_cmd(
|
|
10537
|
+
run_cmd(
|
|
10538
|
+
[
|
|
10539
|
+
"git",
|
|
10540
|
+
"push",
|
|
10541
|
+
"--atomic",
|
|
10542
|
+
"--follow-tags",
|
|
10543
|
+
"origin",
|
|
10544
|
+
f"HEAD:refs/heads/{git_branch}",
|
|
10545
|
+
f"refs/tags/{tag}:refs/tags/{tag}",
|
|
10546
|
+
],
|
|
10547
|
+
cwd=project_dir,
|
|
10548
|
+
env=env,
|
|
10549
|
+
)
|
|
10430
10550
|
|
|
10431
10551
|
success(f"Synced: {repo_name}")
|
|
10432
10552
|
|
|
@@ -11,6 +11,7 @@ Local operations shared by several commands:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import json
|
|
14
15
|
import os
|
|
15
16
|
import pathlib
|
|
16
17
|
import shutil
|
|
@@ -122,6 +123,44 @@ def uv_project_version(
|
|
|
122
123
|
return version.splitlines()[-1].strip()
|
|
123
124
|
|
|
124
125
|
|
|
126
|
+
def uv_preview_patch_version(
|
|
127
|
+
uv_path: pathlib.Path,
|
|
128
|
+
cwd: pathlib.Path,
|
|
129
|
+
env: dict[str, str] | None = None,
|
|
130
|
+
) -> str:
|
|
131
|
+
"""Return the version ``uv version --bump patch`` would produce without mutation."""
|
|
132
|
+
result = subprocess.run(
|
|
133
|
+
[
|
|
134
|
+
str(uv_path),
|
|
135
|
+
"version",
|
|
136
|
+
"--bump",
|
|
137
|
+
"patch",
|
|
138
|
+
"--dry-run",
|
|
139
|
+
"--output-format",
|
|
140
|
+
"json",
|
|
141
|
+
],
|
|
142
|
+
cwd=str(cwd),
|
|
143
|
+
env=env,
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
)
|
|
147
|
+
if result.returncode != 0:
|
|
148
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
149
|
+
suffix = f": {detail}" if detail else ""
|
|
150
|
+
raise RuntimeError(
|
|
151
|
+
f"Command failed ({result.returncode}): {uv_path} version --bump patch "
|
|
152
|
+
f"--dry-run --output-format json{suffix}"
|
|
153
|
+
)
|
|
154
|
+
try:
|
|
155
|
+
payload = json.loads(result.stdout or "")
|
|
156
|
+
except json.JSONDecodeError as exc:
|
|
157
|
+
raise RuntimeError("uv version patch preview returned invalid JSON.") from exc
|
|
158
|
+
version = payload.get("version") if isinstance(payload, dict) else None
|
|
159
|
+
if not isinstance(version, str) or not version.strip():
|
|
160
|
+
raise RuntimeError("uv version patch preview returned no project version.")
|
|
161
|
+
return version.strip()
|
|
162
|
+
|
|
163
|
+
|
|
125
164
|
def git_origin(project_dir: pathlib.Path) -> str:
|
|
126
165
|
"""
|
|
127
166
|
Return git remote origin URL.
|