mainsequence 6.0.37__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.37 → mainsequence-6.0.38}/PKG-INFO +1 -1
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/maintenance/project-maintenance/SKILL.md +20 -7
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/cli.py +163 -42
- {mainsequence-6.0.37 → 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.37 → mainsequence-6.0.38}/mainsequence/client/models_foundry.py +26 -3
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence.egg-info/SOURCES.txt +2 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/pyproject.toml +1 -1
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_cli.py +302 -11
- mainsequence-6.0.38/tests/test_local_ops.py +59 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_pod_project_resolution.py +3 -3
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_project_branch_contract.py +33 -3
- mainsequence-6.0.38/tests/test_ssh_utils.py +187 -0
- mainsequence-6.0.37/mainsequence/cli/ssh_utils.py +0 -184
- {mainsequence-6.0.37 → mainsequence-6.0.38}/LICENSE +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/README.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/data_publishing/meta_table_migrations/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/data_publishing/meta_tables/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/agent_scaffold/skills/sdk_project_execution/SKILL.md +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/__main__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/bootstrap.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/api.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/config.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/migrations.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/project_status.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/cli/ui.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/base.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/client.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/local_paths.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/data_sources_interfaces/sqlite.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/dtype_codec.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/metatables/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/metatables/core.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/models_user.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/client/utils.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/defaults.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/logconf.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/__main__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/compiled_sql/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/compiled_sql/v1.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/build_operations.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/data_nodes.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/models.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/namespacing.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/persist_managers.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/run_operations.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/data_nodes/utils.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/future_registry.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/hashing.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/alembic.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/env.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/provider.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/registry.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/scaffold.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/templates/__init__.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/templates/env.py.mako +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/migrations/templates/script.py.mako +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/pydantic_metadata.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/schema_names.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/meta_tables/sqlalchemy_contracts.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/project_skills.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/runtime_context.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/setup.cfg +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_auth_precedence.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_build_operations_hashing.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_cli_migrations.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_client.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_data_access_mixin_dimension_audit.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_data_node_storage_dimension_queries.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_data_node_update_flow.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_dependency_extras.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_duckdb_interface_dimensions.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_filter_normalization.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_instrumentation.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_logconf.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_meta_table_migrations.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_meta_tables_client_models.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_meta_tables_sqlalchemy_contracts.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_project_batch_jobs_from_file.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_project_skills.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_run_configuration.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_schema_names.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_secret_client_model.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_source_table_configuration.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_sqlite_interface_dimensions.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_update_runner_uid_runtime.py +0 -0
- {mainsequence-6.0.37 → mainsequence-6.0.38}/tests/test_update_statistics.py +0 -0
- {mainsequence-6.0.37 → 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
|
|
@@ -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.
|
|
@@ -9853,24 +9903,24 @@ def project_set_up_locally(
|
|
|
9853
9903
|
target_dir = projects_root / f"{name}-{project_uid}"
|
|
9854
9904
|
projects_root.mkdir(parents=True, exist_ok=True)
|
|
9855
9905
|
|
|
9856
|
-
|
|
9857
|
-
|
|
9906
|
+
if target_dir.exists():
|
|
9907
|
+
warn(f"Target already exists: {target_dir}")
|
|
9908
|
+
raise typer.Exit(2)
|
|
9858
9909
|
|
|
9859
|
-
# Best-effort Project-owned deploy key (do not fail set-up-locally on this)
|
|
9860
9910
|
try:
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
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
|
|
9865
9919
|
|
|
9920
|
+
copied = _copy_clipboard(pub)
|
|
9866
9921
|
agent_env = start_agent_and_add_key(key_path)
|
|
9867
9922
|
|
|
9868
|
-
|
|
9869
|
-
warn(f"Target already exists: {target_dir}")
|
|
9870
|
-
raise typer.Exit(2)
|
|
9871
|
-
|
|
9872
|
-
env = os.environ.copy() | agent_env
|
|
9873
|
-
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)
|
|
9874
9924
|
|
|
9875
9925
|
with status(f"Cloning repo into {target_dir}..."):
|
|
9876
9926
|
rc = subprocess.call(
|
|
@@ -10033,7 +10083,16 @@ def project_open_signed_terminal(
|
|
|
10033
10083
|
|
|
10034
10084
|
origin = git_origin(project_dir)
|
|
10035
10085
|
name = repo_name_from_git_url(origin) or project_dir.name
|
|
10036
|
-
|
|
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
|
|
10037
10096
|
open_signed_terminal(str(project_dir), key_path, name)
|
|
10038
10097
|
|
|
10039
10098
|
|
|
@@ -10300,18 +10359,23 @@ def project_sync(
|
|
|
10300
10359
|
project_id: str | None = typer.Argument(None, help="Project UID"),
|
|
10301
10360
|
path: str | None = typer.Option(None, "--path", help="Project directory"),
|
|
10302
10361
|
message_opt: str | None = typer.Option(None, "--message", "-m", help="Git commit message"),
|
|
10303
|
-
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
|
+
),
|
|
10304
10367
|
):
|
|
10305
10368
|
"""
|
|
10306
10369
|
Run end-to-end sync workflow for project dependencies and git state.
|
|
10307
10370
|
|
|
10308
10371
|
Workflow:
|
|
10309
|
-
1.
|
|
10310
|
-
2.
|
|
10311
|
-
3.
|
|
10312
|
-
4.
|
|
10313
|
-
5.
|
|
10314
|
-
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.
|
|
10315
10379
|
|
|
10316
10380
|
Parameters
|
|
10317
10381
|
----------
|
|
@@ -10322,7 +10386,8 @@ def project_sync(
|
|
|
10322
10386
|
path:
|
|
10323
10387
|
Explicit local path.
|
|
10324
10388
|
dry_run:
|
|
10325
|
-
|
|
10389
|
+
Run read-only preflight and print the plan without generating keys or
|
|
10390
|
+
changing project files, dependencies, Git state, or backend state.
|
|
10326
10391
|
|
|
10327
10392
|
Examples
|
|
10328
10393
|
--------
|
|
@@ -10368,46 +10433,90 @@ def project_sync(
|
|
|
10368
10433
|
f"Git branch {git_branch!r} to ProjectBranch {project_branch_uid}."
|
|
10369
10434
|
)
|
|
10370
10435
|
|
|
10371
|
-
ensure_venv(project_dir)
|
|
10372
|
-
|
|
10373
10436
|
origin = git_origin(project_dir)
|
|
10374
10437
|
repo_name = repo_name_from_git_url(origin) or project_dir.name
|
|
10375
|
-
|
|
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
|
|
10376
10457
|
|
|
10377
10458
|
steps = [
|
|
10378
|
-
"
|
|
10379
|
-
"uv version --bump patch",
|
|
10459
|
+
"preview uv patch version",
|
|
10380
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",
|
|
10381
10468
|
"uv lock",
|
|
10382
10469
|
"uv sync",
|
|
10383
10470
|
"uv export (locked) -> requirements.txt",
|
|
10384
10471
|
"git add -A",
|
|
10385
10472
|
f'git commit -m "{safe_message}"',
|
|
10386
10473
|
"git tag -a <backend tag> -m <backend tag>",
|
|
10387
|
-
"git push --follow-tags",
|
|
10474
|
+
"git push --atomic --follow-tags origin HEAD:refs/heads/<branch> refs/tags/<backend tag>:refs/tags/<backend tag>",
|
|
10388
10475
|
]
|
|
10389
10476
|
|
|
10477
|
+
print_kv(
|
|
10478
|
+
"Sync release",
|
|
10479
|
+
[
|
|
10480
|
+
("Current version", current_version),
|
|
10481
|
+
("Next version", next_version),
|
|
10482
|
+
("Branch tag", tag),
|
|
10483
|
+
],
|
|
10484
|
+
)
|
|
10390
10485
|
print_table("Sync plan", ["Step"], [[s] for s in steps])
|
|
10391
10486
|
|
|
10392
10487
|
if dry_run:
|
|
10393
|
-
warn("Dry run: no
|
|
10488
|
+
warn("Dry run: read-only preflight complete; no changes made.")
|
|
10394
10489
|
return
|
|
10395
10490
|
|
|
10396
|
-
|
|
10397
|
-
|
|
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
|
|
10398
10510
|
|
|
10399
|
-
uv = ensure_uv_installed(project_dir)
|
|
10400
10511
|
with status("Running uv + git sync steps..."):
|
|
10401
10512
|
run_uv(uv, ["version", "--bump", "patch"], cwd=project_dir, env=env)
|
|
10402
10513
|
version = uv_project_version(uv, cwd=project_dir, env=env)
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10514
|
+
if version != next_version:
|
|
10515
|
+
error(
|
|
10516
|
+
f"Project sync version verification failed: uv produced {version}; "
|
|
10517
|
+
f"preflight expected {next_version}."
|
|
10407
10518
|
)
|
|
10408
|
-
|
|
10409
|
-
error(f"Project sync tag resolution failed: {exc}")
|
|
10410
|
-
raise typer.Exit(1) from exc
|
|
10519
|
+
raise typer.Exit(1)
|
|
10411
10520
|
run_uv(uv, ["lock"], cwd=project_dir, env=env)
|
|
10412
10521
|
run_uv(uv, ["sync"], cwd=project_dir, env=env)
|
|
10413
10522
|
# `uv sync` can prune ad hoc packages from `.venv`, including a `uv`
|
|
@@ -10425,7 +10534,19 @@ def project_sync(
|
|
|
10425
10534
|
run_cmd(["git", "add", "-A"], cwd=project_dir, env=env)
|
|
10426
10535
|
run_cmd(["git", "commit", "-m", safe_message], cwd=project_dir, env=env)
|
|
10427
10536
|
run_cmd(["git", "tag", "-a", tag, "-m", tag], cwd=project_dir, env=env)
|
|
10428
|
-
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
|
+
)
|
|
10429
10550
|
|
|
10430
10551
|
success(f"Synced: {repo_name}")
|
|
10431
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.
|