mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7e2368f9e | |||
| c755127317 | |||
| 94c04ee456 | |||
| d4ed02ba74 | |||
| 40ed7129c8 | |||
| c4ef7abff5 | |||
| c75f45f3cf |
@@ -253,6 +253,31 @@ async_client.set_client_factory(your_custom_factory)
|
||||
|
||||
See SPEC-16 for full context manager refactor details.
|
||||
|
||||
### Release Process
|
||||
|
||||
Releases are driven by `just release` / `just beta` — never by a bare `git tag`. The recipes bump version metadata, run pre-flight checks, commit, tag, and push. GitHub Actions then publishes to PyPI and updates the Homebrew formula.
|
||||
|
||||
**Stable release:**
|
||||
|
||||
```
|
||||
just release v0.21.3
|
||||
```
|
||||
|
||||
The recipe runs `just lint` + `just typecheck`, then updates `__version__` in `src/basic_memory/__init__.py` and `"version"` in `server.json` (MCP registry metadata), commits as `chore: update version to X.Y.Z for vX.Y.Z release`, creates the `vX.Y.Z` tag, and pushes both the commit and the tag to `origin/main`. After the tag lands, the `Release` workflow builds the package, publishes to PyPI, creates the GitHub release with auto-generated notes, and updates the Homebrew formula. The recipe finishes by printing the post-release tasks the workflow doesn't cover.
|
||||
|
||||
**Beta release:** `just beta v0.21.3b1` — same flow with a beta-suffixed tag. PyPI consumers install with `pip install basic-memory --pre`.
|
||||
|
||||
**Development builds:** every commit to `main` publishes a `0.21.3.dev26+468a22f`-style version to PyPI automatically via `.github/workflows/dev-release.yml`. No human action.
|
||||
|
||||
**Do not tag releases by hand.** A bare `git tag vX.Y.Z` skips the in-code version bump. Package metadata is still correct (uv-dynamic-versioning derives it from the git tag) but `basic-memory --version` reports the previous release, which is what happened with v0.21.2 → v0.21.3.
|
||||
|
||||
**Post-release tasks** the recipe surfaces but doesn't run:
|
||||
- `docs.basicmemory.com` — add notes to `src/pages/latest-releases.mdx`
|
||||
- `basicmachines.co` — bump version in `src/components/sections/hero.tsx`
|
||||
- MCP Registry — `mcp-publisher publish` from the repo root
|
||||
|
||||
See `.claude/commands/release/release.md` (and `beta.md`, `release-check.md`, `changelog.md` alongside it) for the full release + post-release runbook, including the slash commands.
|
||||
|
||||
## BASIC MEMORY PRODUCT USAGE
|
||||
|
||||
### Knowledge Structure
|
||||
|
||||
@@ -224,40 +224,6 @@ See `test-int/BENCHMARKS.md` for detailed benchmark documentation.
|
||||
- **Fixtures**: Use async pytest fixtures for setup and teardown
|
||||
- **Markers**: Use `@pytest.mark.benchmark` for benchmarks, `@pytest.mark.slow` for slow tests
|
||||
|
||||
## Release Process
|
||||
|
||||
Basic Memory uses automatic versioning based on git tags with `uv-dynamic-versioning`. Here's how releases work:
|
||||
|
||||
### Version Management
|
||||
- **Development versions**: Automatically generated from git commits (e.g., `0.12.4.dev26+468a22f`)
|
||||
- **Beta releases**: Created by tagging with beta suffixes (e.g., `git tag v0.13.0b1`)
|
||||
- **Stable releases**: Created by tagging with version numbers (e.g., `git tag v0.13.0`)
|
||||
|
||||
### Release Workflows
|
||||
|
||||
#### Development Builds
|
||||
- Automatically published to PyPI on every commit to `main`
|
||||
- Version format: `0.12.4.dev26+468a22f` (base version + dev + commit count + hash)
|
||||
- Users install with: `pip install basic-memory --pre --force-reinstall`
|
||||
|
||||
#### Beta Releases
|
||||
1. Create and push a beta tag: `git tag v0.13.0b1 && git push origin v0.13.0b1`
|
||||
2. GitHub Actions automatically builds and publishes to PyPI
|
||||
3. Users install with: `pip install basic-memory --pre`
|
||||
|
||||
#### Stable Releases
|
||||
1. Create and push a version tag: `git tag v0.13.0 && git push origin v0.13.0`
|
||||
2. GitHub Actions automatically:
|
||||
- Builds the package with version `0.13.0`
|
||||
- Creates GitHub release with auto-generated notes
|
||||
- Publishes to PyPI
|
||||
3. Users install with: `pip install basic-memory`
|
||||
|
||||
### For Contributors
|
||||
- No manual version bumping required
|
||||
- Versions are automatically derived from git tags
|
||||
- Focus on code changes, not version management
|
||||
|
||||
## Creating Issues
|
||||
|
||||
If you're planning to work on something, please create an issue first to discuss the approach. Include:
|
||||
|
||||
@@ -123,8 +123,7 @@ phone.
|
||||
time — same files, same format, same wikilinks. Cancel anytime, your data
|
||||
stays yours.
|
||||
|
||||
Built on WorkOS AuthKit, Neon Postgres, and Tigris S3. Platform source at
|
||||
[basic-memory-cloud](https://github.com/basicmachines-co/basic-memory-cloud).
|
||||
Built on WorkOS AuthKit, Neon Postgres, and Tigris S3.
|
||||
|
||||
### Pricing
|
||||
|
||||
|
||||
+2
-2
@@ -6,12 +6,12 @@
|
||||
"url": "https://github.com/basicmachines-co/basic-memory.git",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "0.21.1",
|
||||
"version": "0.21.4",
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "pypi",
|
||||
"identifier": "basic-memory",
|
||||
"version": "0.21.1",
|
||||
"version": "0.21.4",
|
||||
"runtimeHint": "uvx",
|
||||
"runtimeArguments": [
|
||||
{"type": "positional", "value": "basic-memory"},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
# Package version - updated by release automation
|
||||
__version__ = "0.21.1"
|
||||
__version__ = "0.21.4"
|
||||
|
||||
# API version for FastAPI - independent of package version
|
||||
__api_version__ = "v0"
|
||||
|
||||
@@ -447,8 +447,12 @@ class TaskScheduler(Protocol):
|
||||
|
||||
|
||||
def _log_task_failure(completed: asyncio.Task) -> None:
|
||||
if completed.cancelled():
|
||||
return
|
||||
try:
|
||||
completed.result()
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as exc: # pragma: no cover
|
||||
logger.exception("Background task failed", error=str(exc))
|
||||
|
||||
|
||||
@@ -35,11 +35,7 @@ async def write_note(
|
||||
tags: list[str] | str | None = None,
|
||||
note_type: str = "note",
|
||||
metadata: Annotated[dict | None, BeforeValidator(coerce_dict)] = None,
|
||||
# Force/replace are the file-write idioms models default to.
|
||||
overwrite: Annotated[
|
||||
bool | None,
|
||||
Field(default=None, validation_alias=AliasChoices("overwrite", "force", "replace")),
|
||||
] = None,
|
||||
overwrite: bool | None = None,
|
||||
output_format: Literal["text", "json"] = "text",
|
||||
context: Context | None = None,
|
||||
) -> str | dict:
|
||||
|
||||
@@ -149,6 +149,22 @@ def clean_routing_env(monkeypatch) -> None:
|
||||
monkeypatch.delenv("BASIC_MEMORY_EXPLICIT_ROUTING", raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate_data_dir_env(monkeypatch) -> None:
|
||||
"""Keep host data-dir env vars from leaking into integration tests.
|
||||
|
||||
Why: GitHub Actions Ubuntu runners set ``XDG_CONFIG_HOME=/home/runner/.config``,
|
||||
and ``resolve_data_dir()`` honors it ahead of ``Path.home() / ".basic-memory"``.
|
||||
Without clearing it, the MCP tool process reads config.json from the host XDG
|
||||
path instead of the tmp dir the ``config_manager`` fixture wrote to — so
|
||||
``test-project`` is missing from ``config.projects``, ``get_project_mode``
|
||||
falls through to its CLOUD default (#837), and every tool call fails with
|
||||
"Cloud routing requested but no credentials found."
|
||||
"""
|
||||
monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False)
|
||||
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
|
||||
|
||||
|
||||
POSTGRES_EPHEMERAL_TABLES = [
|
||||
"search_vector_embeddings",
|
||||
"search_vector_chunks",
|
||||
|
||||
@@ -310,31 +310,40 @@ async def test_write_note_accepts_directory_aliases(mcp_server, app, test_projec
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_accepts_overwrite_aliases(mcp_server, app, test_project):
|
||||
"""`force`/`replace` should map to `overwrite`."""
|
||||
async def test_write_note_overwrite_canonical_via_mcp(mcp_server, app, test_project):
|
||||
"""Canonical overwrite=True must reach the handler (#818 regression)."""
|
||||
async with Client(mcp_server) as client:
|
||||
# First create
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "Overwrite Alias Note",
|
||||
"title": "Overwrite Canonical Note",
|
||||
"directory": "overwrite-test",
|
||||
"content": "v1",
|
||||
},
|
||||
)
|
||||
# Overwrite using `force` alias
|
||||
blocked = await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "Overwrite Canonical Note",
|
||||
"directory": "overwrite-test",
|
||||
"content": "v2",
|
||||
},
|
||||
)
|
||||
assert "# Error: Note already exists" in blocked.content[0].text
|
||||
|
||||
result = await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "Overwrite Alias Note",
|
||||
"title": "Overwrite Canonical Note",
|
||||
"directory": "overwrite-test",
|
||||
"content": "v2",
|
||||
"force": True, # alias for overwrite
|
||||
"overwrite": True,
|
||||
},
|
||||
)
|
||||
assert "Updated note" in result.content[0].text or "Created note" in result.content[0].text
|
||||
assert "# Updated note" in result.content[0].text
|
||||
|
||||
|
||||
# --- move_note aliases ---
|
||||
@@ -538,3 +547,19 @@ async def test_aliases_not_advertised_in_schema(mcp_server, app):
|
||||
assert canonical in props, f"{tool_name}: canonical '{canonical}' missing"
|
||||
for alias in must_not_have:
|
||||
assert alias not in props, f"{tool_name}: alias '{alias}' leaked into schema"
|
||||
|
||||
# #818: AliasChoices on optional bool broke external-client JSON schema (null-only).
|
||||
overwrite_schema = tools["write_note"].inputSchema["properties"]["overwrite"]
|
||||
schema_types: set[str] = set()
|
||||
if "type" in overwrite_schema:
|
||||
raw = overwrite_schema["type"]
|
||||
if isinstance(raw, str):
|
||||
schema_types.add(raw)
|
||||
else:
|
||||
schema_types.update(raw)
|
||||
for option in overwrite_schema.get("anyOf", ()):
|
||||
if "type" in option:
|
||||
schema_types.add(option["type"])
|
||||
assert "boolean" in schema_types, (
|
||||
f"write_note overwrite must expose boolean in schema, got {overwrite_schema}"
|
||||
)
|
||||
|
||||
@@ -225,6 +225,19 @@ def isolate_routing_env(monkeypatch) -> None:
|
||||
monkeypatch.delenv("BASIC_MEMORY_EXPLICIT_ROUTING", raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate_data_dir_env(monkeypatch) -> None:
|
||||
"""Keep host data-dir env vars from leaking into tests.
|
||||
|
||||
Why: GitHub Actions Ubuntu runners set ``XDG_CONFIG_HOME=/home/runner/.config``,
|
||||
and ``resolve_data_dir()`` honors it ahead of ``Path.home() / ".basic-memory"``.
|
||||
Without clearing it, tests that monkeypatch HOME still see the host XDG path
|
||||
and assertions against the tmp home directory fail.
|
||||
"""
|
||||
monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False)
|
||||
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def cleanup_global_db_after_test() -> AsyncGenerator[None, None]:
|
||||
"""Close any module-level DB engine created outside fixture ownership."""
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Tests for background task done-callback error handling."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory.deps.services import _log_task_failure
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_log_task_failure_ignores_cancelled_task():
|
||||
async def slow():
|
||||
await asyncio.sleep(10)
|
||||
|
||||
task = asyncio.create_task(slow())
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
with patch("basic_memory.deps.services.logger.exception") as mock_exc:
|
||||
_log_task_failure(task)
|
||||
mock_exc.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_log_task_failure_logs_real_exception():
|
||||
async def boom():
|
||||
raise ValueError("sync failed")
|
||||
|
||||
task = asyncio.create_task(boom())
|
||||
with pytest.raises(ValueError):
|
||||
await task
|
||||
|
||||
with patch("basic_memory.deps.services.logger.exception") as mock_exc:
|
||||
_log_task_failure(task)
|
||||
mock_exc.assert_called_once()
|
||||
assert "sync failed" in str(mock_exc.call_args)
|
||||
Reference in New Issue
Block a user