diff --git a/CLAUDE.md b/CLAUDE.md
index ec9250f..66e441b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -171,6 +171,18 @@ Commits should be prefixed with:
Only commits with these prefixes appear in the auto-generated `HISTORY.md`.
+### HISTORY.md Merge Conflicts
+The `HISTORY.md` file is auto-generated when `staging` is merged to `main`. This means:
+- `main` always has the latest HISTORY.md
+- `staging` lags behind until the next release
+- Feature branches created from `main` have the updated history
+
+When merging feature branches to `staging`, conflicts in HISTORY.md are expected. Resolve by accepting the incoming version:
+```bash
+git checkout --theirs HISTORY.md
+git add HISTORY.md
+```
+
### GitHub Actions
- **`publish.yml`**: Triggered on push to `main`, handles versioning and multi-platform publishing
- **`test-pr.yml`**: Runs tests on pull requests
diff --git a/HISTORY.md b/HISTORY.md
index d6bce84..28f2e86 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
## [1.7.0](https://github.com/cortexapps/cli/releases/tag/1.7.0) - 2025-11-19
[Compare with 1.6.0](https://github.com/cortexapps/cli/compare/1.6.0...1.7.0)
@@ -18,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- remove rate limiter initialization log message (#169) #patch ([015107a](https://github.com/cortexapps/cli/commit/015107aca15d5a4cf4eb746834bcbb7dac607e1d) by Jeff Schnitter).
+
## [1.5.0](https://github.com/cortexapps/cli/releases/tag/1.5.0) - 2025-11-13
[Compare with 1.4.0](https://github.com/cortexapps/cli/compare/1.4.0...1.5.0)
diff --git a/cortexapps_cli/commands/backup.py b/cortexapps_cli/commands/backup.py
index 0b39592..2c4fe9d 100644
--- a/cortexapps_cli/commands/backup.py
+++ b/cortexapps_cli/commands/backup.py
@@ -698,3 +698,7 @@ def import_tenant(
print(f"cortex scorecards create -f \"{file_path}\"")
elif import_type == "workflows":
print(f"cortex workflows create -f \"{file_path}\"")
+
+ # Exit with non-zero code if any imports failed
+ if total_failed > 0:
+ raise typer.Exit(1)
diff --git a/cortexapps_cli/cortex_client.py b/cortexapps_cli/cortex_client.py
index 9692efd..abc014f 100644
--- a/cortexapps_cli/cortex_client.py
+++ b/cortexapps_cli/cortex_client.py
@@ -156,8 +156,12 @@ def request(self, method, endpoint, params={}, headers={}, data=None, raw_body=F
print(error_str)
raise typer.Exit(code=1)
except json.JSONDecodeError:
- # if we can't parse the error message, just raise the HTTP error
- response.raise_for_status()
+ # if we can't parse the error message, print a clean error and exit
+ status = response.status_code
+ reason = response.reason or 'Unknown error'
+ error_str = f'[red][bold]HTTP Error {status}[/bold][/red]: {reason}'
+ print(error_str)
+ raise typer.Exit(code=1)
if raw_response:
return response
diff --git a/data/import/entity-types/cli-test.json b/data/import/entity-types/cli-test.json
index 132a29e..0628c4c 100644
--- a/data/import/entity-types/cli-test.json
+++ b/data/import/entity-types/cli-test.json
@@ -1,5 +1,6 @@
{
"description": "This is a test entity type definition.",
+ "iconTag": "Cortex-builtin::Basketball",
"name": "CLI Test With Empty Schema",
"schema": {},
"type": "cli-test"
diff --git a/data/run-time/entity-type-invalid-icon.json b/data/run-time/entity-type-invalid-icon.json
new file mode 100644
index 0000000..7d37ddf
--- /dev/null
+++ b/data/run-time/entity-type-invalid-icon.json
@@ -0,0 +1,7 @@
+{
+ "description": "This is a test entity type definition with invalid icon.",
+ "iconTag": "invalidIcon",
+ "name": "CLI Test With Invalid Icon",
+ "schema": {},
+ "type": "cli-test-invalid-icon"
+}
diff --git a/tests/test_backup.py b/tests/test_backup.py
new file mode 100644
index 0000000..be9c138
--- /dev/null
+++ b/tests/test_backup.py
@@ -0,0 +1,40 @@
+from tests.helpers.utils import *
+import os
+import tempfile
+
+def test_backup_import_invalid_api_key(monkeypatch):
+ """
+ Test that backup import exits with non-zero return code when API calls fail.
+ """
+ monkeypatch.setenv("CORTEX_API_KEY", "invalidKey")
+
+ # Create a temp directory with a catalog subdirectory and a simple yaml file
+ with tempfile.TemporaryDirectory() as tmpdir:
+ catalog_dir = os.path.join(tmpdir, "catalog")
+ os.makedirs(catalog_dir)
+
+ # Create a minimal catalog entity file
+ entity_file = os.path.join(catalog_dir, "test-entity.yaml")
+ with open(entity_file, "w") as f:
+ f.write("""
+info:
+ x-cortex-tag: test-entity
+ title: Test Entity
+ x-cortex-type: service
+""")
+
+ result = cli(["backup", "import", "-d", tmpdir], return_type=ReturnType.RAW)
+ assert result.exit_code != 0, f"backup import should exit with non-zero code on failure, got exit_code={result.exit_code}"
+
+
+def test_backup_export_invalid_api_key(monkeypatch):
+ """
+ Test that backup export exits with non-zero return code and clean error message when API calls fail.
+ """
+ monkeypatch.setenv("CORTEX_API_KEY", "invalidKey")
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ result = cli(["backup", "export", "-d", tmpdir], return_type=ReturnType.RAW)
+ assert result.exit_code != 0, f"backup export should exit with non-zero code on failure, got exit_code={result.exit_code}"
+ assert "HTTP Error 401" in result.stdout, "Should show HTTP 401 error message"
+ assert "Traceback" not in result.stdout, "Should not show Python traceback"
diff --git a/tests/test_config_file.py b/tests/test_config_file.py
index f37cc81..1f86c97 100644
--- a/tests/test_config_file.py
+++ b/tests/test_config_file.py
@@ -31,12 +31,12 @@ def test_config_file_bad_api_key(monkeypatch, tmp_path):
monkeypatch.setattr('sys.stdin', io.StringIO('y'))
f = tmp_path / "test-config-bad-api-key.txt"
response = cli(["-c", str(f), "-k", "invalidApiKey", "scorecards", "list"], return_type=ReturnType.RAW)
- assert "401 Client Error: Unauthorized" in str(response), "should get Unauthorized error"
+ assert "HTTP Error 401" in response.stdout, "should get Unauthorized error"
def test_environment_variable_invalid_key(monkeypatch):
monkeypatch.setenv("CORTEX_API_KEY", "invalidKey")
response = cli(["scorecards", "list"], return_type=ReturnType.RAW)
- assert "401 Client Error: Unauthorized" in str(response), "should get Unauthorized error"
+ assert "HTTP Error 401" in response.stdout, "should get Unauthorized error"
def test_config_file_bad_url(monkeypatch, tmp_path):
monkeypatch.delenv("CORTEX_BASE_URL")
diff --git a/tests/test_entity_types.py b/tests/test_entity_types.py
index 8b2d65b..c5b59fd 100644
--- a/tests/test_entity_types.py
+++ b/tests/test_entity_types.py
@@ -12,6 +12,18 @@ def test_resource_definitions(capsys):
response = cli(["entity-types", "list"])
assert any(definition['type'] == 'cli-test' for definition in response['definitions']), "Should find entity type named 'cli-test'"
- cli(["entity-types", "get", "-t", "cli-test"])
+ # Verify iconTag was set correctly
+ response = cli(["entity-types", "get", "-t", "cli-test"])
+ assert response.get('iconTag') == "Cortex-builtin::Basketball", "iconTag should be set to Cortex-builtin::Basketball"
cli(["entity-types", "update", "-t", "cli-test", "-f", "data/run-time/entity-type-update.json"])
+
+
+def test_resource_definitions_invalid_icon():
+ # API does not reject invalid iconTag values - it uses a default icon instead
+ # This test verifies that behavior and will catch if the API changes to reject invalid icons
+ response = cli(["entity-types", "create", "-f", "data/run-time/entity-type-invalid-icon.json"], return_type=ReturnType.RAW)
+ assert response.exit_code == 0, "Creation should succeed even with invalid iconTag (API uses default icon)"
+
+ # Clean up the test entity type
+ cli(["entity-types", "delete", "-t", "cli-test-invalid-icon"])
diff --git a/tests/test_scim.py b/tests/test_scim.py
index 3215747..ed5bda7 100644
--- a/tests/test_scim.py
+++ b/tests/test_scim.py
@@ -2,6 +2,7 @@
from urllib.error import HTTPError
import pytest
+@pytest.mark.skip(reason="Disabled until CET-23082 is resolved.")
def test():
response = cli(["scim", "list"], ReturnType.STDOUT)