Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/test-daily.yml
Original file line number Diff line number Diff line change
@@ -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
83 changes: 70 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,25 +139,54 @@ Use the GitHub-recommended format: `<issue-number>-<short-description>`
- 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 <last-tag>..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).

Expand All @@ -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:
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<!-- insertion marker -->
## [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

<small>[Compare with 1.8.0](https://github.com/cortexapps/cli/compare/1.8.0...1.9.0)</small>
<small>[Compare with 1.9.0](https://github.com/cortexapps/cli/compare/1.9.0...1.10.0)</small>

## [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

<small>[Compare with 1.7.0](https://github.com/cortexapps/cli/compare/1.7.0...1.8.0)</small>
<small>[Compare with 1.7.0](https://github.com/cortexapps/cli/compare/1.7.0...1.9.0)</small>

### Bug Fixes

Expand Down
7 changes: 7 additions & 0 deletions cortexapps_cli/cortex_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib.metadata
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Expand Down Expand Up @@ -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__)

Expand Down Expand Up @@ -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('/')])
Expand Down
5 changes: 5 additions & 0 deletions data/run-time/catalog-delete-by-type-entity.yaml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions data/run-time/entity-type-delete-by-type.json
Original file line number Diff line number Diff line change
@@ -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"
}
20 changes: 20 additions & 0 deletions tests/test_catalog_delete_by_type.py
Original file line number Diff line number Diff line change
@@ -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']}"
1 change: 1 addition & 0 deletions tests/test_catalog_list_by_owners_multiple.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions tests/test_catalog_list_by_owners_single.py
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 20 additions & 0 deletions tests/test_user_agent.py
Original file line number Diff line number Diff line change
@@ -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}"
Loading