veris-cli 2.25.1__tar.gz → 2.25.2__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.
- {veris_cli-2.25.1 → veris_cli-2.25.2}/PKG-INFO +2 -1
- {veris_cli-2.25.1 → veris_cli-2.25.2}/pyproject.toml +2 -1
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/api.py +47 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/env.py +124 -81
- {veris_cli-2.25.1 → veris_cli-2.25.2}/.gitignore +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/README.md +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/__init__.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/build_context.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/cli.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/__init__.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/_helpers.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/auth.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/evaluations.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/profile.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/reports.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/run.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/scenarios.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/simulations.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/config.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/output.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/prompts.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/run_output.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/scripts/__init__.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_build.sh +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_push.sh +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/searchable_checkbox.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/templates.py +0 -0
- {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/veris_yaml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: veris-cli
|
|
3
|
-
Version: 2.25.
|
|
3
|
+
Version: 2.25.2
|
|
4
4
|
Summary: CLI to connect local agents to the Veris backend
|
|
5
5
|
Project-URL: Homepage, https://github.com/veris-ai/veris-cli
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/veris-ai/veris-cli/issues
|
|
@@ -13,6 +13,7 @@ Requires-Dist: pydantic>=2.6
|
|
|
13
13
|
Requires-Dist: pyyaml>=6.0.1
|
|
14
14
|
Requires-Dist: questionary>=2.0.0
|
|
15
15
|
Requires-Dist: rich>=13.7.0
|
|
16
|
+
Requires-Dist: tenacity>=8.2.0
|
|
16
17
|
Provides-Extra: dev
|
|
17
18
|
Requires-Dist: pre-commit>=4.3.0; extra == 'dev'
|
|
18
19
|
Provides-Extra: test
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "veris-cli"
|
|
3
|
-
version = "2.25.
|
|
3
|
+
version = "2.25.2"
|
|
4
4
|
description = "CLI to connect local agents to the Veris backend"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
"questionary>=2.0.0",
|
|
16
16
|
"pyyaml>=6.0.1",
|
|
17
17
|
"pydantic>=2.6",
|
|
18
|
+
"tenacity>=8.2.0",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
[project.scripts]
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
from typing import Any, Optional
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
|
+
from tenacity import (
|
|
7
|
+
retry,
|
|
8
|
+
retry_if_exception,
|
|
9
|
+
retry_if_exception_type,
|
|
10
|
+
stop_after_attempt,
|
|
11
|
+
wait_exponential,
|
|
12
|
+
)
|
|
6
13
|
|
|
7
14
|
from veris_cli.config import Config
|
|
8
15
|
|
|
@@ -29,6 +36,26 @@ def _raise_for_status(response: httpx.Response) -> None:
|
|
|
29
36
|
raise APIError(response.status_code, detail, str(response.url))
|
|
30
37
|
|
|
31
38
|
|
|
39
|
+
_RETRYABLE_STATUSES = frozenset({502, 503, 504})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_transient_api_error(exc: BaseException) -> bool:
|
|
43
|
+
return isinstance(exc, APIError) and exc.status_code in _RETRYABLE_STATUSES
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Retries transient failures (network errors, 502/503/504). Only applied to
|
|
47
|
+
# idempotent reads — mutating calls (POST/PUT/DELETE) are left alone to avoid
|
|
48
|
+
# duplicate side effects on retry.
|
|
49
|
+
_retry_transient = retry(
|
|
50
|
+
reraise=True,
|
|
51
|
+
stop=stop_after_attempt(5),
|
|
52
|
+
wait=wait_exponential(multiplier=1, min=1, max=10),
|
|
53
|
+
retry=(
|
|
54
|
+
retry_if_exception_type(httpx.TransportError) | retry_if_exception(_is_transient_api_error)
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
32
59
|
class VerisAPI:
|
|
33
60
|
"""Simple HTTP client for Veris backend API."""
|
|
34
61
|
|
|
@@ -66,6 +93,7 @@ class VerisAPI:
|
|
|
66
93
|
_raise_for_status(response)
|
|
67
94
|
return response.json()
|
|
68
95
|
|
|
96
|
+
@_retry_transient
|
|
69
97
|
def list_environments(
|
|
70
98
|
self, status: Optional[str] = None, limit: int = 20, offset: int = 0
|
|
71
99
|
) -> dict[str, Any]:
|
|
@@ -81,6 +109,7 @@ class VerisAPI:
|
|
|
81
109
|
_raise_for_status(response)
|
|
82
110
|
return response.json()
|
|
83
111
|
|
|
112
|
+
@_retry_transient
|
|
84
113
|
def get_environment(self, environment_id: str) -> dict[str, Any]:
|
|
85
114
|
"""Get environment details."""
|
|
86
115
|
with httpx.Client(
|
|
@@ -124,6 +153,7 @@ class VerisAPI:
|
|
|
124
153
|
_raise_for_status(response)
|
|
125
154
|
return response.json()
|
|
126
155
|
|
|
156
|
+
@_retry_transient
|
|
127
157
|
def get_build_status(self, environment_id: str, build_id: str) -> dict[str, Any]:
|
|
128
158
|
"""Poll build status."""
|
|
129
159
|
with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=30) as client:
|
|
@@ -133,6 +163,7 @@ class VerisAPI:
|
|
|
133
163
|
_raise_for_status(response)
|
|
134
164
|
return response.json()
|
|
135
165
|
|
|
166
|
+
@_retry_transient
|
|
136
167
|
def get_build_logs(
|
|
137
168
|
self, environment_id: str, build_id: str, cursor: str | None = None
|
|
138
169
|
) -> dict[str, Any]:
|
|
@@ -169,6 +200,7 @@ class VerisAPI:
|
|
|
169
200
|
_raise_for_status(response)
|
|
170
201
|
|
|
171
202
|
# Scenario Sets
|
|
203
|
+
@_retry_transient
|
|
172
204
|
def list_scenario_sets(
|
|
173
205
|
self, environment_id: Optional[str] = None, limit: int = 100, skip: int = 0
|
|
174
206
|
) -> list[dict[str, Any]]:
|
|
@@ -189,6 +221,7 @@ class VerisAPI:
|
|
|
189
221
|
else data
|
|
190
222
|
)
|
|
191
223
|
|
|
224
|
+
@_retry_transient
|
|
192
225
|
def get_scenario_set(self, set_id: str) -> dict[str, Any]:
|
|
193
226
|
"""Get scenario set details."""
|
|
194
227
|
with httpx.Client(
|
|
@@ -232,6 +265,7 @@ class VerisAPI:
|
|
|
232
265
|
_raise_for_status(response)
|
|
233
266
|
return response.json()
|
|
234
267
|
|
|
268
|
+
@_retry_transient
|
|
235
269
|
def list_runs(
|
|
236
270
|
self,
|
|
237
271
|
status: Optional[str] = None,
|
|
@@ -253,6 +287,7 @@ class VerisAPI:
|
|
|
253
287
|
_raise_for_status(response)
|
|
254
288
|
return response.json()
|
|
255
289
|
|
|
290
|
+
@_retry_transient
|
|
256
291
|
def get_run(self, run_id: str) -> dict[str, Any]:
|
|
257
292
|
"""Get run details."""
|
|
258
293
|
with httpx.Client(
|
|
@@ -270,6 +305,7 @@ class VerisAPI:
|
|
|
270
305
|
response = client.delete(f"/v1/runs/{run_id}")
|
|
271
306
|
_raise_for_status(response)
|
|
272
307
|
|
|
308
|
+
@_retry_transient
|
|
273
309
|
def get_run_events(self, run_id: str, limit: int = 100, offset: int = 0) -> dict[str, Any]:
|
|
274
310
|
"""Get run events/logs."""
|
|
275
311
|
params = {"limit": limit, "offset": offset}
|
|
@@ -280,6 +316,7 @@ class VerisAPI:
|
|
|
280
316
|
_raise_for_status(response)
|
|
281
317
|
return response.json()
|
|
282
318
|
|
|
319
|
+
@_retry_transient
|
|
283
320
|
def list_run_simulations(self, run_id: str) -> dict[str, Any]:
|
|
284
321
|
"""List simulations for a run."""
|
|
285
322
|
with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=30) as client:
|
|
@@ -307,6 +344,7 @@ class VerisAPI:
|
|
|
307
344
|
return response.json()
|
|
308
345
|
|
|
309
346
|
# Graders
|
|
347
|
+
@_retry_transient
|
|
310
348
|
def list_graders(
|
|
311
349
|
self,
|
|
312
350
|
environment_id: str,
|
|
@@ -340,6 +378,7 @@ class VerisAPI:
|
|
|
340
378
|
_raise_for_status(response)
|
|
341
379
|
return response.json()
|
|
342
380
|
|
|
381
|
+
@_retry_transient
|
|
343
382
|
def list_evaluation_runs(self, run_id: str, limit: int = 20, offset: int = 0) -> dict[str, Any]:
|
|
344
383
|
"""List evaluation runs for a given run."""
|
|
345
384
|
params = {"limit": limit, "offset": offset}
|
|
@@ -350,6 +389,7 @@ class VerisAPI:
|
|
|
350
389
|
_raise_for_status(response)
|
|
351
390
|
return response.json()
|
|
352
391
|
|
|
392
|
+
@_retry_transient
|
|
353
393
|
def get_evaluation_run(self, run_id: str, eval_run_id: str) -> dict[str, Any]:
|
|
354
394
|
"""Get evaluation run details including per-simulation results."""
|
|
355
395
|
with httpx.Client(
|
|
@@ -360,6 +400,7 @@ class VerisAPI:
|
|
|
360
400
|
return response.json()
|
|
361
401
|
|
|
362
402
|
# Environment Variables
|
|
403
|
+
@_retry_transient
|
|
363
404
|
def list_environment_variables(self, environment_id: str) -> dict[str, Any]:
|
|
364
405
|
"""List environment variables for an environment."""
|
|
365
406
|
with httpx.Client(
|
|
@@ -404,6 +445,7 @@ class VerisAPI:
|
|
|
404
445
|
_raise_for_status(response)
|
|
405
446
|
return response.json()
|
|
406
447
|
|
|
448
|
+
@_retry_transient
|
|
407
449
|
def list_reports(self, environment_id: str, limit: int = 20, offset: int = 0) -> dict[str, Any]:
|
|
408
450
|
"""List reports for an environment."""
|
|
409
451
|
params: dict[str, Any] = {
|
|
@@ -418,6 +460,7 @@ class VerisAPI:
|
|
|
418
460
|
_raise_for_status(response)
|
|
419
461
|
return response.json()
|
|
420
462
|
|
|
463
|
+
@_retry_transient
|
|
421
464
|
def get_report(self, report_id: str) -> dict[str, Any]:
|
|
422
465
|
"""Get report details including html_content."""
|
|
423
466
|
with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=60) as client:
|
|
@@ -425,6 +468,7 @@ class VerisAPI:
|
|
|
425
468
|
_raise_for_status(response)
|
|
426
469
|
return response.json()
|
|
427
470
|
|
|
471
|
+
@_retry_transient
|
|
428
472
|
def download_report(self, report_id: str, format: str) -> bytes:
|
|
429
473
|
"""Download report in the specified format (pdf, md)."""
|
|
430
474
|
with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=120) as client:
|
|
@@ -449,6 +493,7 @@ class VerisAPI:
|
|
|
449
493
|
_raise_for_status(response)
|
|
450
494
|
return response.json()
|
|
451
495
|
|
|
496
|
+
@_retry_transient
|
|
452
497
|
def get_managed_config(self, environment_id: str) -> dict[str, Any]:
|
|
453
498
|
"""Get managed config files (signed download URLs) for an environment."""
|
|
454
499
|
with httpx.Client(
|
|
@@ -459,6 +504,7 @@ class VerisAPI:
|
|
|
459
504
|
return response.json()
|
|
460
505
|
|
|
461
506
|
# Sandbox metadata
|
|
507
|
+
@_retry_transient
|
|
462
508
|
def list_services(self) -> dict[str, Any]:
|
|
463
509
|
"""List available sandbox mock services."""
|
|
464
510
|
with httpx.Client(
|
|
@@ -468,6 +514,7 @@ class VerisAPI:
|
|
|
468
514
|
_raise_for_status(response)
|
|
469
515
|
return response.json()
|
|
470
516
|
|
|
517
|
+
@_retry_transient
|
|
471
518
|
def list_channels(self) -> dict[str, Any]:
|
|
472
519
|
"""List available channel types."""
|
|
473
520
|
with httpx.Client(
|
|
@@ -22,6 +22,63 @@ from veris_cli.config import Config, ProjectConfig
|
|
|
22
22
|
from veris_cli.veris_yaml import DEFAULT_DOCKERFILE, VerisYaml, slugify
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _check_sync_guard(
|
|
26
|
+
env_info: dict,
|
|
27
|
+
pc: ProjectConfig,
|
|
28
|
+
force: bool,
|
|
29
|
+
) -> str | None:
|
|
30
|
+
"""Check if remote managed config diverges from local and prompt user.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
"push" — user chose to overwrite remote with local config
|
|
34
|
+
"pull" — user chose to pull remote config
|
|
35
|
+
None — guard didn't fire (proceed normally)
|
|
36
|
+
|
|
37
|
+
Exits via sys.exit(1) in non-interactive mode when divergence is detected.
|
|
38
|
+
Raises SystemExit on cancel.
|
|
39
|
+
"""
|
|
40
|
+
if env_info.get("status") != "ready" or force:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
remote_config_at = env_info.get("config_updated_at")
|
|
44
|
+
local_synced_at = pc.get_config_synced_at()
|
|
45
|
+
if not remote_config_at or (local_synced_at and remote_config_at <= local_synced_at):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
if not sys.stdin.isatty():
|
|
49
|
+
output.print_error(
|
|
50
|
+
"Remote config was updated since your last sync. "
|
|
51
|
+
"Run 'veris env config pull' first, or pass --force to overwrite."
|
|
52
|
+
)
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
import questionary
|
|
56
|
+
|
|
57
|
+
if not local_synced_at:
|
|
58
|
+
sync_message = "Your managed setup is complete. New config is available on the server."
|
|
59
|
+
else:
|
|
60
|
+
sync_message = "Remote config was updated since your last sync."
|
|
61
|
+
|
|
62
|
+
action = questionary.select(
|
|
63
|
+
sync_message,
|
|
64
|
+
choices=[
|
|
65
|
+
questionary.Choice(title="Pull remote config (overwrites local)", value="pull"),
|
|
66
|
+
questionary.Choice(title="Push local config (overwrites remote)", value="push"),
|
|
67
|
+
questionary.Choice(title="Cancel", value="cancel"),
|
|
68
|
+
],
|
|
69
|
+
).ask()
|
|
70
|
+
if action == "cancel" or action is None:
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
if action == "push":
|
|
73
|
+
confirm = questionary.confirm(
|
|
74
|
+
"This will overwrite the managed config generated by the Veris team. Continue?",
|
|
75
|
+
default=False,
|
|
76
|
+
).ask()
|
|
77
|
+
if not confirm:
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
return action
|
|
80
|
+
|
|
81
|
+
|
|
25
82
|
def _env_push_local(
|
|
26
83
|
env_id: str,
|
|
27
84
|
tag: str,
|
|
@@ -439,6 +496,10 @@ def env_create(ctx, name: str | None, agent_name: str | None):
|
|
|
439
496
|
if target_obj is not None:
|
|
440
497
|
try:
|
|
441
498
|
api.upload_environment_config(env_id, target_obj.to_upload_dict())
|
|
499
|
+
# Record sync timestamp so `env push` sync guard stays in sync.
|
|
500
|
+
# We just uploaded from this machine — no need to fetch the
|
|
501
|
+
# server timestamp; "now" is accurate enough.
|
|
502
|
+
project_config.set_config_synced_at(datetime.now(UTC).isoformat())
|
|
442
503
|
output.print_success("Config uploaded")
|
|
443
504
|
except Exception as e:
|
|
444
505
|
output.print_warning(f"Config upload failed (non-fatal): {e}")
|
|
@@ -714,7 +775,7 @@ def env_submit(ctx, env_id: str | None, target: str | None):
|
|
|
714
775
|
api.notify_submit_uploaded(env_id)
|
|
715
776
|
output.print_success("Submitted for managed setup!")
|
|
716
777
|
output.print_info("You'll receive an email when your environment is ready.")
|
|
717
|
-
output.print_info("Then run 'veris env
|
|
778
|
+
output.print_info("Then run 'veris env config pull' followed by 'veris env push'.")
|
|
718
779
|
|
|
719
780
|
except ValueError as e:
|
|
720
781
|
output.print_error(str(e))
|
|
@@ -791,59 +852,62 @@ def env_push(ctx, tag: str, env_id: str | None, force: bool, target: str | None)
|
|
|
791
852
|
|
|
792
853
|
api = VerisAPI(profile=profile)
|
|
793
854
|
|
|
794
|
-
|
|
795
|
-
output.print_error(
|
|
796
|
-
f"{dockerfile_rel} not found. Run 'veris env create' or 'veris env config pull' first."
|
|
797
|
-
)
|
|
798
|
-
sys.exit(1)
|
|
799
|
-
|
|
800
|
-
# Config sync guard: only managed-setup envs (status=ready) have config
|
|
801
|
-
# that could be authored by someone other than this CLI user. For self-serve
|
|
802
|
-
# envs (status=created), the user IS the author — no divergence is possible
|
|
803
|
-
# and the guard would only cause spurious CI failures (local config_synced_at
|
|
804
|
-
# is always missing on fresh runners). status=pending is already blocked by
|
|
805
|
-
# the backend with its own message.
|
|
855
|
+
# Fetch env info early — needed for Dockerfile check and sync guard.
|
|
806
856
|
try:
|
|
807
857
|
env_info = api.get_environment(env_id)
|
|
808
858
|
except Exception:
|
|
809
859
|
env_info = {}
|
|
810
860
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
861
|
+
if not dockerfile_path.exists():
|
|
862
|
+
env_status = env_info.get("status", "")
|
|
863
|
+
if env_status == "pending":
|
|
864
|
+
output.print_error(
|
|
865
|
+
"Your environment is still being set up. You'll receive an email when it's ready."
|
|
866
|
+
)
|
|
867
|
+
sys.exit(1)
|
|
868
|
+
elif env_status == "ready":
|
|
869
|
+
# Config is ready on the server — offer to pull it.
|
|
817
870
|
if sys.stdin.isatty():
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if action == "cancel" or action is None:
|
|
833
|
-
return
|
|
834
|
-
if action == "pull":
|
|
835
|
-
ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
|
|
836
|
-
# Reload target config after pull (resolved_target stays the same)
|
|
837
|
-
parsed = _load_veris_yaml(veris_yaml_path)
|
|
838
|
-
target_obj = parsed.get_target(resolved_target)
|
|
839
|
-
dockerfile_rel = target_obj.resolved_dockerfile()
|
|
840
|
-
dockerfile_path = Path.cwd() / dockerfile_rel
|
|
871
|
+
output.print_info(
|
|
872
|
+
"Dockerfile not found locally, but config is ready on the server."
|
|
873
|
+
)
|
|
874
|
+
pulled = ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
|
|
875
|
+
if not pulled:
|
|
876
|
+
sys.exit(1)
|
|
877
|
+
# Reload after pull
|
|
878
|
+
parsed = _load_veris_yaml(veris_yaml_path)
|
|
879
|
+
target_obj = parsed.get_target(resolved_target)
|
|
880
|
+
dockerfile_rel = target_obj.resolved_dockerfile()
|
|
881
|
+
dockerfile_path = Path.cwd() / dockerfile_rel
|
|
882
|
+
if not dockerfile_path.exists():
|
|
883
|
+
output.print_error("Dockerfile still missing after config pull.")
|
|
884
|
+
sys.exit(1)
|
|
841
885
|
else:
|
|
842
886
|
output.print_error(
|
|
843
|
-
"
|
|
844
|
-
"Run 'veris env config pull' first, or pass --force to overwrite."
|
|
887
|
+
"Dockerfile not found. Run 'veris env config pull' to download it."
|
|
845
888
|
)
|
|
846
889
|
sys.exit(1)
|
|
890
|
+
else:
|
|
891
|
+
output.print_error(
|
|
892
|
+
"Dockerfile not found. Create it locally, or run 'veris env submit' "
|
|
893
|
+
"for managed setup."
|
|
894
|
+
)
|
|
895
|
+
sys.exit(1)
|
|
896
|
+
|
|
897
|
+
# Config sync guard: only managed-setup envs (status=ready) have config
|
|
898
|
+
# that could be authored by someone other than this CLI user.
|
|
899
|
+
pc = ProjectConfig(profile=profile, target=resolved_target)
|
|
900
|
+
guard_action = _check_sync_guard(env_info, pc, force)
|
|
901
|
+
if guard_action == "pull":
|
|
902
|
+
pulled = ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
|
|
903
|
+
if not pulled:
|
|
904
|
+
output.print_error("Config pull did not download files. Aborting push.")
|
|
905
|
+
sys.exit(1)
|
|
906
|
+
# Reload target config after pull (resolved_target stays the same)
|
|
907
|
+
parsed = _load_veris_yaml(veris_yaml_path)
|
|
908
|
+
target_obj = parsed.get_target(resolved_target)
|
|
909
|
+
dockerfile_rel = target_obj.resolved_dockerfile()
|
|
910
|
+
dockerfile_path = Path.cwd() / dockerfile_rel
|
|
847
911
|
|
|
848
912
|
try:
|
|
849
913
|
api.upload_environment_config(env_id, target_obj.to_upload_dict())
|
|
@@ -1036,46 +1100,16 @@ def config_push(ctx, file_path: str | None, env_id: str, force: bool, target: st
|
|
|
1036
1100
|
api = VerisAPI(profile=profile)
|
|
1037
1101
|
pc = ProjectConfig(profile=profile, target=resolved_target)
|
|
1038
1102
|
|
|
1039
|
-
# Config sync guard — only meaningful for managed-setup envs (status=ready)
|
|
1040
|
-
# where someone else may have authored the remote config. See env_push for
|
|
1041
|
-
# the full rationale.
|
|
1042
1103
|
try:
|
|
1043
1104
|
env_info = api.get_environment(env_id)
|
|
1044
1105
|
except Exception:
|
|
1045
1106
|
env_info = {}
|
|
1046
1107
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
import questionary
|
|
1053
|
-
|
|
1054
|
-
action = questionary.select(
|
|
1055
|
-
"Remote config was updated since your last sync.",
|
|
1056
|
-
choices=[
|
|
1057
|
-
questionary.Choice(
|
|
1058
|
-
title="Pull remote config (overwrites local)", value="pull"
|
|
1059
|
-
),
|
|
1060
|
-
questionary.Choice(
|
|
1061
|
-
title="Push local config (overwrites remote)", value="push"
|
|
1062
|
-
),
|
|
1063
|
-
questionary.Choice(title="Cancel", value="cancel"),
|
|
1064
|
-
],
|
|
1065
|
-
).ask()
|
|
1066
|
-
if action == "cancel" or action is None:
|
|
1067
|
-
return
|
|
1068
|
-
if action == "pull":
|
|
1069
|
-
# Delegate to config pull logic
|
|
1070
|
-
output.print_info("Pulling remote config...")
|
|
1071
|
-
ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
|
|
1072
|
-
return
|
|
1073
|
-
else:
|
|
1074
|
-
output.print_error(
|
|
1075
|
-
"Remote config was updated since your last sync. "
|
|
1076
|
-
"Run 'veris env config pull' first, or pass --force to overwrite."
|
|
1077
|
-
)
|
|
1078
|
-
sys.exit(1)
|
|
1108
|
+
guard_action = _check_sync_guard(env_info, pc, force)
|
|
1109
|
+
if guard_action == "pull":
|
|
1110
|
+
output.print_info("Pulling remote config...")
|
|
1111
|
+
ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
|
|
1112
|
+
return
|
|
1079
1113
|
|
|
1080
1114
|
try:
|
|
1081
1115
|
api.upload_environment_config(env_id, target_obj.to_upload_dict())
|
|
@@ -1118,8 +1152,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
|
|
|
1118
1152
|
"Your environment is being set up. You'll receive an email when it's ready."
|
|
1119
1153
|
)
|
|
1120
1154
|
else:
|
|
1121
|
-
output.print_info(
|
|
1122
|
-
|
|
1155
|
+
output.print_info(
|
|
1156
|
+
"No remote config available yet. "
|
|
1157
|
+
"If using managed setup, run 'veris env submit'."
|
|
1158
|
+
)
|
|
1159
|
+
return False
|
|
1123
1160
|
|
|
1124
1161
|
veris_dir = Path.cwd() / ".veris"
|
|
1125
1162
|
veris_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1134,6 +1171,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
|
|
|
1134
1171
|
output.print_warning("Failed to download Dockerfile.sandbox")
|
|
1135
1172
|
|
|
1136
1173
|
veris_yaml_url = result.get("veris_yaml_url")
|
|
1174
|
+
if veris_yaml_url and not resolved_target:
|
|
1175
|
+
output.print_warning(
|
|
1176
|
+
"Skipped veris.yaml download — no target resolved. "
|
|
1177
|
+
"Use --target or set an active target with 'veris env targets set <name>'."
|
|
1178
|
+
)
|
|
1137
1179
|
if veris_yaml_url and resolved_target:
|
|
1138
1180
|
resp = httpx.get(veris_yaml_url, timeout=60)
|
|
1139
1181
|
if resp.status_code == 200:
|
|
@@ -1155,6 +1197,7 @@ def config_pull(ctx, env_id: str | None, target: str | None):
|
|
|
1155
1197
|
env_detail = api.get_environment(env_id)
|
|
1156
1198
|
config_ts = env_detail.get("config_updated_at") or datetime.now(UTC).isoformat()
|
|
1157
1199
|
ProjectConfig(profile=profile, target=resolved_target).set_config_synced_at(config_ts)
|
|
1200
|
+
return True
|
|
1158
1201
|
|
|
1159
1202
|
except Exception as e:
|
|
1160
1203
|
output.print_error(f"Failed to pull config: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|