veris-cli 2.25.0__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.0 → veris_cli-2.25.2}/PKG-INFO +2 -1
- {veris_cli-2.25.0 → veris_cli-2.25.2}/pyproject.toml +2 -1
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/api.py +47 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/env.py +137 -63
- {veris_cli-2.25.0 → veris_cli-2.25.2}/.gitignore +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/README.md +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/__init__.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/build_context.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/cli.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/__init__.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/_helpers.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/auth.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/evaluations.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/profile.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/reports.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/run.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/scenarios.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/simulations.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/config.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/output.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/prompts.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/run_output.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/scripts/__init__.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_build.sh +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_push.sh +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/searchable_checkbox.py +0 -0
- {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/templates.py +0 -0
- {veris_cli-2.25.0 → 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))
|
|
@@ -730,9 +791,16 @@ def env_submit(ctx, env_id: str | None, target: str | None):
|
|
|
730
791
|
@env.command(name="push")
|
|
731
792
|
@click.option("--tag", default="latest", help="Image tag (default: latest)")
|
|
732
793
|
@click.option("--env-id", default=None, help="Environment ID (overrides config)")
|
|
794
|
+
@click.option(
|
|
795
|
+
"--force",
|
|
796
|
+
"-f",
|
|
797
|
+
is_flag=True,
|
|
798
|
+
default=False,
|
|
799
|
+
help="Skip the sync guard for managed-setup envs (use in CI).",
|
|
800
|
+
)
|
|
733
801
|
@target_option
|
|
734
802
|
@click.pass_context
|
|
735
|
-
def env_push(ctx, tag: str, env_id: str | None, target: str | None):
|
|
803
|
+
def env_push(ctx, tag: str, env_id: str | None, force: bool, target: str | None):
|
|
736
804
|
"""Build and push environment image to Veris.
|
|
737
805
|
|
|
738
806
|
Resolves the target's block from veris.yaml (including an optional
|
|
@@ -784,50 +852,63 @@ def env_push(ctx, tag: str, env_id: str | None, target: str | None):
|
|
|
784
852
|
|
|
785
853
|
api = VerisAPI(profile=profile)
|
|
786
854
|
|
|
787
|
-
|
|
788
|
-
output.print_error(
|
|
789
|
-
f"{dockerfile_rel} not found. Run 'veris env create' or 'veris env config pull' first."
|
|
790
|
-
)
|
|
791
|
-
sys.exit(1)
|
|
792
|
-
|
|
793
|
-
# Config sync guard: check if remote config was updated since last sync.
|
|
794
|
-
# Prevents accidentally overwriting team-generated config with local scaffold.
|
|
855
|
+
# Fetch env info early — needed for Dockerfile check and sync guard.
|
|
795
856
|
try:
|
|
796
857
|
env_info = api.get_environment(env_id)
|
|
797
858
|
except Exception:
|
|
798
859
|
env_info = {}
|
|
799
860
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
return
|
|
818
|
-
if action == "pull":
|
|
819
|
-
ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
|
|
820
|
-
# Reload target config after pull (resolved_target stays the same)
|
|
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.
|
|
870
|
+
if sys.stdin.isatty():
|
|
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
|
|
821
878
|
parsed = _load_veris_yaml(veris_yaml_path)
|
|
822
879
|
target_obj = parsed.get_target(resolved_target)
|
|
823
880
|
dockerfile_rel = target_obj.resolved_dockerfile()
|
|
824
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)
|
|
885
|
+
else:
|
|
886
|
+
output.print_error(
|
|
887
|
+
"Dockerfile not found. Run 'veris env config pull' to download it."
|
|
888
|
+
)
|
|
889
|
+
sys.exit(1)
|
|
825
890
|
else:
|
|
826
891
|
output.print_error(
|
|
827
|
-
"
|
|
892
|
+
"Dockerfile not found. Create it locally, or run 'veris env submit' "
|
|
893
|
+
"for managed setup."
|
|
828
894
|
)
|
|
829
895
|
sys.exit(1)
|
|
830
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
|
|
911
|
+
|
|
831
912
|
try:
|
|
832
913
|
api.upload_environment_config(env_id, target_obj.to_upload_dict())
|
|
833
914
|
# Re-fetch to get the server-assigned config_updated_at
|
|
@@ -982,9 +1063,16 @@ def config():
|
|
|
982
1063
|
help="Path to veris.yaml (default: .veris/veris.yaml)",
|
|
983
1064
|
)
|
|
984
1065
|
@click.option("--env-id", default=None, help="Environment ID (uses project config if omitted)")
|
|
1066
|
+
@click.option(
|
|
1067
|
+
"--force",
|
|
1068
|
+
"-f",
|
|
1069
|
+
is_flag=True,
|
|
1070
|
+
default=False,
|
|
1071
|
+
help="Skip the sync guard for managed-setup envs (use in CI).",
|
|
1072
|
+
)
|
|
985
1073
|
@target_option
|
|
986
1074
|
@click.pass_context
|
|
987
|
-
def config_push(ctx, file_path: str | None, env_id: str, target: str | None):
|
|
1075
|
+
def config_push(ctx, file_path: str | None, env_id: str, force: bool, target: str | None):
|
|
988
1076
|
"""Upload veris.yaml config to the backend without building an image.
|
|
989
1077
|
|
|
990
1078
|
Reads .veris/veris.yaml by default, or a custom path via --file. With
|
|
@@ -1012,39 +1100,16 @@ def config_push(ctx, file_path: str | None, env_id: str, target: str | None):
|
|
|
1012
1100
|
api = VerisAPI(profile=profile)
|
|
1013
1101
|
pc = ProjectConfig(profile=profile, target=resolved_target)
|
|
1014
1102
|
|
|
1015
|
-
# Config sync guard
|
|
1016
1103
|
try:
|
|
1017
1104
|
env_info = api.get_environment(env_id)
|
|
1018
1105
|
except Exception:
|
|
1019
1106
|
env_info = {}
|
|
1020
1107
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
import questionary
|
|
1027
|
-
|
|
1028
|
-
action = questionary.select(
|
|
1029
|
-
"Remote config was updated since your last sync.",
|
|
1030
|
-
choices=[
|
|
1031
|
-
questionary.Choice(title="Pull remote config (overwrites local)", value="pull"),
|
|
1032
|
-
questionary.Choice(title="Push local config (overwrites remote)", value="push"),
|
|
1033
|
-
questionary.Choice(title="Cancel", value="cancel"),
|
|
1034
|
-
],
|
|
1035
|
-
).ask()
|
|
1036
|
-
if action == "cancel" or action is None:
|
|
1037
|
-
return
|
|
1038
|
-
if action == "pull":
|
|
1039
|
-
# Delegate to config pull logic
|
|
1040
|
-
output.print_info("Pulling remote config...")
|
|
1041
|
-
ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
|
|
1042
|
-
return
|
|
1043
|
-
else:
|
|
1044
|
-
output.print_error(
|
|
1045
|
-
"Remote config was updated since your last sync. Run 'veris env config pull' first."
|
|
1046
|
-
)
|
|
1047
|
-
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
|
|
1048
1113
|
|
|
1049
1114
|
try:
|
|
1050
1115
|
api.upload_environment_config(env_id, target_obj.to_upload_dict())
|
|
@@ -1087,8 +1152,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
|
|
|
1087
1152
|
"Your environment is being set up. You'll receive an email when it's ready."
|
|
1088
1153
|
)
|
|
1089
1154
|
else:
|
|
1090
|
-
output.print_info(
|
|
1091
|
-
|
|
1155
|
+
output.print_info(
|
|
1156
|
+
"No remote config available yet. "
|
|
1157
|
+
"If using managed setup, run 'veris env submit'."
|
|
1158
|
+
)
|
|
1159
|
+
return False
|
|
1092
1160
|
|
|
1093
1161
|
veris_dir = Path.cwd() / ".veris"
|
|
1094
1162
|
veris_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1103,6 +1171,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
|
|
|
1103
1171
|
output.print_warning("Failed to download Dockerfile.sandbox")
|
|
1104
1172
|
|
|
1105
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
|
+
)
|
|
1106
1179
|
if veris_yaml_url and resolved_target:
|
|
1107
1180
|
resp = httpx.get(veris_yaml_url, timeout=60)
|
|
1108
1181
|
if resp.status_code == 200:
|
|
@@ -1124,6 +1197,7 @@ def config_pull(ctx, env_id: str | None, target: str | None):
|
|
|
1124
1197
|
env_detail = api.get_environment(env_id)
|
|
1125
1198
|
config_ts = env_detail.get("config_updated_at") or datetime.now(UTC).isoformat()
|
|
1126
1199
|
ProjectConfig(profile=profile, target=resolved_target).set_config_synced_at(config_ts)
|
|
1200
|
+
return True
|
|
1127
1201
|
|
|
1128
1202
|
except Exception as e:
|
|
1129
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
|