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
17 changes: 14 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
pull_request:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -34,7 +33,7 @@ jobs:
run: uv build

publish:
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
name: Publish to PyPI
needs: test
runs-on: ubuntu-latest
Expand Down Expand Up @@ -62,6 +61,18 @@ jobs:
- name: Build Distributions
run: uv build

- name: Check PyPI Version
id: pypi_version
run: |
VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

if curl -fs "https://pypi.org/pypi/ctxd/$VERSION/json" > /dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

- name: Publish Distributions to PyPI
if: ${{ steps.pypi_version.outputs.exists == 'false' }}
uses: pypa/gh-action-pypi-publish@release/v1

61 changes: 61 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Contributing

## Bumping the Package Version

The package version is defined in `pyproject.toml` in two places:

- `project.version`
- `tool.bumpversion.current_version`

PyPI does not allow re-publishing the same version. Any PR that should publish a
new package must bump both values to the next version before merging.

### Manual Version Bump

For a patch release, update `pyproject.toml`:

```toml
[project]
version = "0.1.14"

[tool.bumpversion]
current_version = "0.1.14"
```

Then run the test suite:

```bash
uv run pytest tests -v
```

Commit the version bump with the code change:

```bash
git add pyproject.toml
git commit -m "Bump version to 0.1.14"
```

### Using bump-my-version

This repository includes `tool.bumpversion` configuration. If
`bump-my-version` is installed, you can bump the patch version with:

```bash
uv tool run bump-my-version bump patch
```

Review the resulting `pyproject.toml` change before committing it.

### Publishing

Publishing is handled by GitHub Actions. Pull requests run tests and build the
package, but do not publish to PyPI.

After a PR is merged, a push to `main` runs the full test job. If the tests pass,
the publish job checks whether the current version already exists on PyPI:

- If the version does not exist, the package is published.
- If the version already exists, publishing is skipped.

Do not push release tags to publish. Releases are published from `main` after
the merge commit passes CI.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,8 @@ from ctxd import AsyncClient
async with AsyncClient(api_key="<api-key>") as client:
results = await client.search("text:deployment")
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for the package version bump and release
process.
29 changes: 28 additions & 1 deletion src/ctxd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import getpass
import json
import os
import re
import sys
import webbrowser
from typing import Sequence
Expand All @@ -30,7 +31,7 @@ def main(argv: Sequence[str] | None = None) -> int:
client = Client()

if args.command == "search":
result = client.search(" ".join(args.query))
result = client.search(_normalize_search_query(args.query))
return _emit_result(result.model_dump(), as_json=True)
if args.command == "fetch":
result = client.fetch_document(args.document_uid)
Expand Down Expand Up @@ -153,6 +154,32 @@ def _build_parser() -> argparse.ArgumentParser:
return parser


def _normalize_search_query(query_tokens: Sequence[str]) -> str:
normalized_tokens = [
_quote_shell_stripped_text_token(token) for token in query_tokens
]
return " ".join(normalized_tokens)


def _quote_shell_stripped_text_token(token: str) -> str:
if not token.lower().startswith("text:"):
return token

value = token[5:]
if not value or not re.search(r"\s", value):
return token

stripped_value = value.strip()
if (
stripped_value.startswith(("\"", "'", "("))
or stripped_value.endswith(("\"", "'", ")"))
):
return token

escaped_value = stripped_value.replace("\\", "\\\\").replace('"', '\\"')
return f'text:"{escaped_value}"'


def _handle_login(args: argparse.Namespace) -> int:
del args
api_key, should_save = _resolve_login_api_key()
Expand Down
24 changes: 24 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,30 @@ def test_cli_search_accepts_unquoted_query_tokens() -> None:
assert '"results": []' in stdout.getvalue()


def test_cli_search_restores_shell_stripped_text_quotes() -> None:
stdout = StringIO()

with patch(
"ctxd.cli.Client.search",
return_value=type(
"SearchResultLike",
(),
{
"model_dump": lambda self: {
"results": [],
"error": None,
"dsl_parse_error": None,
}
},
)(),
) as search, redirect_stdout(stdout):
exit_code = main(["search", "text:deployment process", "application:slack"])

assert exit_code == 0
search.assert_called_once_with('text:"deployment process" application:slack')
assert '"results": []' in stdout.getvalue()


def test_cli_search_outputs_json_for_empty_success() -> None:
stdout = StringIO()

Expand Down
Loading