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)