diff --git a/.github/workflows/test-daily.yml b/.github/workflows/test-daily.yml new file mode 100644 index 0000000..2e4de24 --- /dev/null +++ b/.github/workflows/test-daily.yml @@ -0,0 +1,55 @@ +name: Daily Test + +on: + workflow_dispatch: + schedule: + - cron: '0 15 * * 1-5' + +env: + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + CORTEX_API_KEY: ${{ secrets.CORTEX_API_KEY }} + CORTEX_API_KEY_VIEWER: ${{ secrets.CORTEX_API_KEY_VIEWER }} + CORTEX_BASE_URL: ${{ vars.CORTEX_BASE_URL }} + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + - name: Install dependencies + run: | + sudo apt update && sudo apt install just + python -m pip install --upgrade pip + pip install poetry poetry-audit-plugin pytest-cov pytest pytest-xdist + + - name: Run pip-audit to check for vulnerabilities + run: poetry audit + + - name: Create and populate .cortex/config file + run: | + mkdir $HOME/.cortex + echo "[default]" > $HOME/.cortex/config + echo "api_key = $CORTEX_API_KEY" >> $HOME/.cortex/config + echo "base_url = $CORTEX_BASE_URL" >> $HOME/.cortex/config + shell: bash + + - name: Install package + run: | + poetry build + poetry install + + - name: Test with pytest + run: | + just test-all diff --git a/CLAUDE.md b/CLAUDE.md index 1cd9996..b0217e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,25 +139,54 @@ Use the GitHub-recommended format: `-` - Use lowercase kebab-case for the description - Keep the description concise (3-5 words) +### Direct Commits to Main +Documentation-only changes (like updates to CLAUDE.md, README.md, STYLE.md) can be committed directly to `main` without going through the staging workflow. + ### Release Workflow 1. Create feature branch for changes 2. Create PR to merge feature branch to `staging` for testing 3. Create PR to merge `staging` to `main` to trigger release: ```bash - gh pr create --base main --head staging --title "Release X.Y.Z: Description #patch|#minor|#major" + gh pr create --base main --head staging --title "Release X.Y.Z: Description" ``` - - Include version number and brief description in title - - Use `#patch`, `#minor`, or `#major` in the title to control version bump + - Include the expected version number and brief description in title - List all changes in the PR body -4. Version bumping (based on hashtag in PR title or commit message): - - Default: Patch version bump - - `#minor`: Minor version bump - - `#major`: Major version bump +4. Version bumping is automatic based on **conventional commit prefixes** in the commit history since the last tag: + - `feat:` prefix → **minor** version bump (new features) + - `fix:` prefix → **patch** version bump (bug fixes) + - If multiple types present, the highest wins (feat > fix) + - Default (no recognized prefix): patch bump 5. Release publishes to: - PyPI - Docker Hub (`cortexapp/cli:VERSION` and `cortexapp/cli:latest`) - Homebrew tap (`cortexapps/homebrew-tap`) +### Determining the Next Version (Claude Instructions) +Before creating a staging-to-main release PR, Claude must: + +1. **Check the current version tag**: + ```bash + git fetch origin --tags + git describe --tags --abbrev=0 + ``` + +2. **Analyze commits since the last tag**: + ```bash + git log ..origin/staging --oneline + ``` + +3. **Determine the version bump** by examining commit prefixes: + - Any `feat:` commit → minor bump (X.Y.0 → X.(Y+1).0) + - Only `fix:` commits → patch bump (X.Y.Z → X.Y.(Z+1)) + +4. **Explain the version** to the user before creating the PR: + > "The next version will be **X.Y.Z** because the commits include: + > - `feat: add --table option to newrelic list` (triggers minor bump) + > - `fix: correct API endpoint for validate` + > Since there's a `feat:` commit, this will be a minor version bump." + +This ensures the user understands what version will be published and why. + ### Homebrew Dependency Updates The `mislav/bump-homebrew-formula-action` only updates the main package URL and SHA256. It **cannot** update the `resource` blocks for Python dependencies (this is a documented limitation of the action). @@ -169,13 +198,31 @@ When updating Python dependency versions (e.g., urllib3, requests), the homebrew **Important**: The `homebrew/cortexapps-cli.rb` file in this repository should be kept in sync with the tap formula for reference. Update it when making dependency changes. ### Commit Message Format -Commits should be prefixed with: -- `add`: New features -- `fix`: Bug fixes -- `change`: Changes to existing features -- `remove`: Removing features +Commits use **conventional commit** prefixes which affect both versioning and changelog: -Only commits with these prefixes appear in the auto-generated `HISTORY.md`. +**For version bumping** (detected by github-tag-action): +- `feat:` → triggers **minor** version bump +- `fix:` → triggers **patch** version bump + +**For HISTORY.md changelog** (detected by git-changelog): +- `add:` → appears under "Added" +- `fix:` → appears under "Bug Fixes" +- `change:` → appears under "Changed" +- `remove:` → appears under "Removed" + +Best practice: Use `feat:` for new features (will bump minor) and `fix:` for bug fixes (will bump patch). These prefixes satisfy both the version bumping and changelog generation. + +### 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 +``` ### HISTORY.md Merge Conflicts The `HISTORY.md` file is auto-generated when `staging` is merged to `main`. This means: @@ -193,6 +240,16 @@ git add HISTORY.md - **`publish.yml`**: Triggered on push to `main`, handles versioning and multi-platform publishing - **`test-pr.yml`**: Runs tests on pull requests +### Branch Protection on `main` +The `main` branch has push restrictions enabled to control who can merge: +- **Allowed users**: `jeff-schnitter` +- **Allowed apps**: `github-actions` (so `publish.yml` can push HISTORY.md updates) +- **No PR requirement**: Disabled so that doc-only commits and CI-generated HISTORY.md changes can be pushed directly +- **Force pushes**: Blocked +- **Branch deletion**: Blocked + +External contributors can fork the repo and open PRs, but only allowed users can merge or push to `main`. + ## Key Files - `cli.py`: Main CLI entry point and global callback diff --git a/HISTORY.md b/HISTORY.md index 089da2e..872e27b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,13 +6,13 @@ 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.9.0](https://github.com/cortexapps/cli/releases/tag/1.9.0) - 2026-01-12 +## [1.10.0](https://github.com/cortexapps/cli/releases/tag/1.10.0) - 2026-01-23 -[Compare with 1.8.0](https://github.com/cortexapps/cli/compare/1.8.0...1.9.0) +[Compare with 1.9.0](https://github.com/cortexapps/cli/compare/1.9.0...1.10.0) -## [1.8.0](https://github.com/cortexapps/cli/releases/tag/1.8.0) - 2026-01-12 +## [1.9.0](https://github.com/cortexapps/cli/releases/tag/1.9.0) - 2026-01-12 -[Compare with 1.7.0](https://github.com/cortexapps/cli/compare/1.7.0...1.8.0) +[Compare with 1.7.0](https://github.com/cortexapps/cli/compare/1.7.0...1.9.0) ### Bug Fixes diff --git a/cortexapps_cli/cortex_client.py b/cortexapps_cli/cortex_client.py index 5a7e349..320edc1 100644 --- a/cortexapps_cli/cortex_client.py +++ b/cortexapps_cli/cortex_client.py @@ -1,3 +1,4 @@ +import importlib.metadata import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry @@ -71,6 +72,11 @@ def __init__(self, api_key, tenant, numeric_level, base_url='https://api.getcort self.tenant = tenant self.base_url = base_url + try: + self.version = importlib.metadata.version('cortexapps_cli') + except importlib.metadata.PackageNotFoundError: + self.version = 'unknown' + logging.basicConfig(level=numeric_level) self.logger = logging.getLogger(__name__) @@ -110,6 +116,7 @@ def request(self, method, endpoint, params={}, headers={}, data=None, raw_body=F req_headers = { 'Authorization': f'Bearer {self.api_key}', 'Content-Type': content_type, + 'User-Agent': f'cortexapps-cli/{self.version}', **headers } url = '/'.join([self.base_url.rstrip('/'), endpoint.lstrip('/')]) diff --git a/data/run-time/catalog-delete-by-type-entity.yaml b/data/run-time/catalog-delete-by-type-entity.yaml new file mode 100644 index 0000000..b33be91 --- /dev/null +++ b/data/run-time/catalog-delete-by-type-entity.yaml @@ -0,0 +1,5 @@ +openapi: 3.0.1 +info: + title: Delete By Type Test Entity + x-cortex-tag: cli-test-delete-by-type-entity + x-cortex-type: cli-test-delete-by-type diff --git a/data/run-time/entity-type-delete-by-type.json b/data/run-time/entity-type-delete-by-type.json new file mode 100644 index 0000000..8af27db --- /dev/null +++ b/data/run-time/entity-type-delete-by-type.json @@ -0,0 +1,6 @@ +{ + "description": "Temporary entity type for testing delete-by-type.", + "name": "CLI Test Delete By Type", + "schema": {}, + "type": "cli-test-delete-by-type" +} diff --git a/tests/test_catalog_delete_by_type.py b/tests/test_catalog_delete_by_type.py new file mode 100644 index 0000000..d043caa --- /dev/null +++ b/tests/test_catalog_delete_by_type.py @@ -0,0 +1,20 @@ +from tests.helpers.utils import * + +@pytest.mark.skip(reason="Disabled until CET-24425 is resolved.") +def test(): + # Create the entity type + cli(["entity-types", "create", "-f", "data/run-time/entity-type-delete-by-type.json"], ReturnType.RAW) + + # Create an entity of that type + cli(["catalog", "create", "-f", "data/run-time/catalog-delete-by-type-entity.yaml"]) + + # Verify the entity exists + response = cli(["catalog", "list", "-t", "cli-test-delete-by-type"]) + assert response['total'] > 0, "Should find at least 1 entity of type 'cli-test-delete-by-type'" + + # Delete all entities of that type + cli(["catalog", "delete-by-type", "-t", "cli-test-delete-by-type"]) + + # Verify 0 entities remain of that type + response = cli(["catalog", "list", "-t", "cli-test-delete-by-type"]) + assert response['total'] == 0, f"Expected 0 entities of x-cortex-type:cli-test-delete-by-type, but found: {response['total']}" diff --git a/tests/test_catalog_list_by_owners_multiple.py b/tests/test_catalog_list_by_owners_multiple.py index efaba14..b888c8d 100644 --- a/tests/test_catalog_list_by_owners_multiple.py +++ b/tests/test_catalog_list_by_owners_multiple.py @@ -1,5 +1,6 @@ from tests.helpers.utils import * +@pytest.mark.skip(reason="Disabled until CET-24479 is resolved.") def test(): response = cli(["catalog", "list", "-o", "cli-test-team-1,cli-test-team-2"]) assert (response['total'] == 2) diff --git a/tests/test_catalog_list_by_owners_single.py b/tests/test_catalog_list_by_owners_single.py index 4ba18c2..faa8735 100644 --- a/tests/test_catalog_list_by_owners_single.py +++ b/tests/test_catalog_list_by_owners_single.py @@ -1,5 +1,6 @@ from tests.helpers.utils import * +@pytest.mark.skip(reason="Disabled until CET-24479 is resolved.") def test(): response = cli(["catalog", "list", "-o", "cli-test-team-1"]) assert (response['total'] == 1) diff --git a/tests/test_user_agent.py b/tests/test_user_agent.py new file mode 100644 index 0000000..56b63b1 --- /dev/null +++ b/tests/test_user_agent.py @@ -0,0 +1,20 @@ +from tests.helpers.utils import * + +@responses.activate +def test_user_agent_header_is_set(): + """Verify that all API requests include a User-Agent header identifying the CLI.""" + responses.add(responses.GET, os.getenv("CORTEX_BASE_URL") + "/api/v1/catalog", json={"entities": [], "total": 0, "page": 0, "totalPages": 0}, status=200) + cli(["catalog", "list", "-p", "0"]) + assert len(responses.calls) == 1 + user_agent = responses.calls[0].request.headers.get("User-Agent", "") + assert user_agent.startswith("cortexapps-cli/") + +@responses.activate +def test_user_agent_header_contains_version(): + """Verify that the User-Agent header contains the package version.""" + import importlib.metadata + expected_version = importlib.metadata.version('cortexapps_cli') + responses.add(responses.GET, os.getenv("CORTEX_BASE_URL") + "/api/v1/catalog", json={"entities": [], "total": 0, "page": 0, "totalPages": 0}, status=200) + cli(["catalog", "list", "-p", "0"]) + user_agent = responses.calls[0].request.headers.get("User-Agent", "") + assert user_agent == f"cortexapps-cli/{expected_version}"