diff --git a/AGENTS.md b/AGENTS.md index 9355867..db713ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,9 +44,10 @@ haystack-enterprise deploy pipeline.py SERVICE Redirect with `--output`, never with `>`. The CLI's logger writes to stdout, so a shell redirect interleaves warning lines into the YAML or JSON and the result no longer parses. -Exit codes worth knowing: 1 for validation, transform, and API errors; 130 when `run` is interrupted; -and 0 when a `deploy` rollout is detached or times out. A zero exit from `deploy` does not mean the -service is serving — confirm with `haystack-enterprise service-status SERVICE`. +Exit codes worth knowing: 1 for validation, transform, and API errors, and for a `deploy` rollout that +fails or does not finish within the wait limit; 130 when `run` is interrupted; and 0 when you detach from +a `deploy` on purpose (Ctrl-C or `--skip-activation`). A rollout that timed out may still come up — +confirm with `haystack-enterprise service-status SERVICE`. Set `CI=true` to replace spinners and progress bars with plain line output. diff --git a/docs/guides/deploy-a-pipeline.md b/docs/guides/deploy-a-pipeline.md index bd6e1a8..76ccfe6 100644 --- a/docs/guides/deploy-a-pipeline.md +++ b/docs/guides/deploy-a-pipeline.md @@ -228,8 +228,8 @@ A pipeline with conventionally named sockets (`query`, `answers`, `replies`, `do inference and asks nothing. Pin the mapping in `.io.yaml` (or `--io-config`) to skip the questions entirely; `--share` reviews the whole mapping instead (see below). -If you interrupt the command (Ctrl-C) during rollout, the rollout continues on the platform. Check -progress any time with: +If you interrupt the command (Ctrl-C) during rollout, the rollout continues on the platform and the +command exits 0, since you detached on purpose. Check progress any time with: ```shell haystack-enterprise service-status my-service @@ -407,6 +407,19 @@ appends a new line per repaint, so a multi-minute rollout becomes thousands of l Set `CI=false` (or unset it) to force the animations back on. Independently of `CI`, `run` never animates when its stdout is redirected, so a piped JSON payload stays parseable. +### What the job result means + +The exit code of `deploy` answers "is the new revision serving?", so a CI job that passes can be trusted: + +- **Exit 0**: the revision is serving (a serverless service activated it, or a managed rollout reached + `DEPLOYED` or `IDLE`), or you chose not to activate it with `--skip-activation`, which pushes the revision + and says so. Interrupting with Ctrl-C also exits 0. +- **Exit 1**: the rollout failed, **or it did not finish within the wait limit** (10 minutes). In the second + case the rollout continues on the platform and the service may still come up; check with + `haystack-enterprise service-status my-service`, then re-run the job or fix the service. + +To push a revision without waiting for a rollout, use `--skip-activation` and activate it later. + To pass CI secrets to the service, expose them to the step's environment and name them with a bare `--env`. A GitHub Actions example: diff --git a/haystack_enterprise_sdk/cli.py b/haystack_enterprise_sdk/cli.py index a1dc61a..964e511 100644 --- a/haystack_enterprise_sdk/cli.py +++ b/haystack_enterprise_sdk/cli.py @@ -1010,7 +1010,8 @@ def _on_status(status: object) -> None: ) elif result.timed_out: typer.echo( - f"Activation of '{service_name}' is still in progress. Detached; the rollout continues. " + f"The rollout of '{service_name}' did not finish within the wait limit, so the new revision is " + f"not confirmed to be serving. The rollout continues on the platform. " f"Check with `haystack-enterprise service-status {service_name}`." ) elif result.deployment.deployment_mode is DeploymentMode.SERVERLESS: @@ -1030,6 +1031,10 @@ def _on_status(status: object) -> None: ) _echo_endpoint(client, service_name, result, shared=bool(share)) + # A job result answers "is the new revision serving?", so an unfinished rollout must not look like success. + if result.timed_out: + raise typer.Exit(1) + @cli_app.command() def validate( diff --git a/tests/unit/test_cli_deploy.py b/tests/unit/test_cli_deploy.py index ba2f415..3c43e9a 100644 --- a/tests/unit/test_cli_deploy.py +++ b/tests/unit/test_cli_deploy.py @@ -146,8 +146,8 @@ def test_deploy_activates_by_default(self, client_cls: Mock) -> None: def test_deploy_activate_timed_out(self, client_cls: Mock) -> None: client_cls.return_value.deploy.return_value = _result(activated=True, timed_out=True) result = runner.invoke(cli_app, ["deploy", FIXTURE, "svc"]) - assert result.exit_code == 0 - assert "still in progress" in result.stdout + assert result.exit_code == 1 + assert "did not finish within the wait limit" in result.stdout @patch("haystack_enterprise_sdk.cli.DeploymentClient") def test_deploy_failure_exits_1(self, client_cls: Mock) -> None: @@ -455,7 +455,7 @@ def test_deploy_share_skipped_when_not_deployed(self, client_cls: Mock) -> None: # Rollout timed out: the service never reached DEPLOYED, so the prototype must not be attempted. client_cls.return_value.deploy.return_value = _result(activated=True, timed_out=True) result = runner.invoke(cli_app, ["deploy", FIXTURE, "svc", "--share"]) - assert result.exit_code == 0 + assert result.exit_code == 1 assert "Skipped shared prototype" in result.stdout client_cls.return_value.create_shared_prototype.assert_not_called() @@ -496,7 +496,7 @@ def test_no_endpoint_when_rollout_timed_out(self, client_cls: Mock) -> None: _stub_endpoint(client_cls) client_cls.return_value.deploy.return_value = _result(activated=True, timed_out=True) result = runner.invoke(cli_app, ["deploy", FIXTURE, "svc"]) - assert result.exit_code == 0 + assert result.exit_code == 1 assert "chat/completions" not in result.stdout @patch("haystack_enterprise_sdk.cli.DeploymentClient")