From a6189c1e92f66852173c835e42e4484cd1895a75 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:28:41 +0000 Subject: [PATCH 01/13] Increment Version to 0.2.1 --- hivemind_sqlite_database/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hivemind_sqlite_database/version.py b/hivemind_sqlite_database/version.py index 58d7648..0343953 100644 --- a/hivemind_sqlite_database/version.py +++ b/hivemind_sqlite_database/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 4 +VERSION_ALPHA = 0 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From ed7fb8aa18d2adcef0cfb846bd6493e78d612043 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:20:46 +0100 Subject: [PATCH 02/13] feat: add SQLCipher encryption support (password kwarg) (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: replace hand-rolled release jobs with shared template inputs The old publish_stable.yml and release_workflow.yml extracted the version by importing hivemind_sqlite_database, which triggered __init__.py and pulled in ovos_utils — not present in the bare workflow environment. Replace all duplicated publish_pypi, sync_dev, propose_release, and notify jobs with the equivalent inputs on the shared publish-stable.yml@dev and publish-alpha.yml@dev reusable workflows, which use get_version.py to read version.py directly without any package import. AI-Generated Change: - Model: claude-sonnet-4-6 - Intent: fix version extraction failure in CI release workflows - Impact: replaced publish_stable.yml, release_workflow.yml - Verified via: reviewed diff against shared template inputs Co-Authored-By: Claude Sonnet 4.6 * feat: add optional SQLCipher encryption to SQLiteDB via password kwarg AI-Generated Change: - Model: Claude Sonnet 4.6 - Intent: allow operators to encrypt the client database at rest using SQLCipher AES-256 - Impact: SQLiteDB gains a password field; when set, sqlcipher3 is used as the sqlite3 backend and PRAGMA key is issued immediately after connect; when None (default) the existing stdlib sqlite3 path is unchanged; ImportError with install hint raised if sqlcipher3 is missing but a password is supplied - Verified via: python -m pytest tests/test_sqlitedb.py -q -p no:ovoscope (31 passed) * chore: add cipher optional-dependency extra for sqlcipher3 AI-Generated Change: - Model: Claude Sonnet 4.6 - Intent: let users opt into SQLCipher encryption via pip install hivemind-sqlite-database[cipher] - Impact: pyproject.toml gains [project.optional-dependencies] cipher = ["sqlcipher3"] - Verified via: python -m pytest tests/test_sqlitedb.py -q -p no:ovoscope (31 passed) * test: add SQLCipher encrypted-path tests (skipped without sqlcipher3) AI-Generated Change: - Model: Claude Sonnet 4.6 - Intent: verify encrypted path works end-to-end and existing CI remains green without sqlcipher3 - Impact: TestSQLiteDBEncrypted (3 tests, skipped when sqlcipher3 absent) and TestSQLiteDBMissingCipher (ImportError when sqlcipher3 not installed) added - Verified via: python -m pytest tests/test_sqlitedb.py -q -p no:ovoscope (35 passed with sqlcipher3; 32 passed + 3 skipped without) * docs: document SQLCipher encryption in README and add CI workflow AI-Generated Change: - Model: Claude Sonnet 4.6 - Intent: document the password kwarg, install steps, and data-loss warning; add CI that tests both plain and encrypted paths - Impact: README.md rewritten with usage examples, apt/pip install instructions, and data-loss warning; .github/workflows/tests.yml added with two jobs (plain sqlite3 and sqlcipher3) - Verified via: python -m pytest tests/test_sqlitedb.py -q -p no:ovoscope (35 passed) * fix(ci): remove libsqlcipher0 from apt install — not present on Ubuntu 24.04 Ubuntu Noble (24.04) replaced libsqlcipher0 with libsqlcipher1. Installing libsqlcipher-dev is sufficient as it declares a dependency on libsqlcipher1 and pulls it in automatically. AI-Generated Change: - Model: claude-sonnet-4-6 - Intent: fix SQLite + SQLCipher CI job failing with exit code 100 - Impact: `apt-get install -y libsqlcipher0` → removed; libsqlcipher-dev alone satisfies the requirement - Verified via: gh run log inspection showing "Unable to locate package libsqlcipher0" Co-Authored-By: Claude Sonnet 4.6 * fix: address CodeRabbit Major issues in encryption implementation AI-Generated Change: - Model: claude-sonnet-4-6 - Intent: address CodeRabbit Major review comments on PR #24 Changes: - __init__.py: gate on `password is not None` instead of truthiness so empty-string passwords don't silently downgrade to plaintext; raise ValueError for empty strings - __init__.py: escape single-quotes in passphrase before building PRAGMA key string (SQLite double-quote escaping) to avoid broken opens and eliminate an injection surface - tests/test_sqlitedb.py: rewrite _make_encrypted_db() to drive through __post_init__() (patching xdg_data_home) instead of reimplementing the SQLCipher setup, so regressions in the production path are caught - tests/test_sqlitedb.py: remove unused `importlib` import (Ruff F401 / CI lint failure) Verified via: python -m pytest tests/test_sqlitedb.py -q (35 passed) python -m ruff check (all checks passed) Co-Authored-By: Claude Sonnet 4.6 * fix(ci): address CodeRabbit workflow issues AI-Generated Change: - Model: claude-sonnet-4-6 - Intent: address CodeRabbit Critical/Major review comments on CI workflows Changes: - tests.yml: add `sudo apt-get update` before apt install to prevent intermittent "Unable to locate package" failures on stale runner indexes - tests.yml: remove broken `|| pip install -e . && pip install sqlcipher3` fallback that masked a misconfigured cipher extra; now fails loudly if `.[cipher]` is broken (cipher extra is defined in pyproject.toml so this is the correct install path) - release_workflow.yml: add `if: github.event.pull_request.merged == true` guard to publish_alpha job so closing a PR without merging does not trigger PyPI publish, release proposal, or Matrix notification Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/tests.yml | 36 ++++++++++++ README.md | 77 +++++++++++++++++++++++- hivemind_sqlite_database/__init__.py | 32 +++++++++- pyproject.toml | 3 + tests/test_sqlitedb.py | 87 ++++++++++++++++++++++++++++ 5 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3a7e54b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + test-plain: + name: "SQLite (no encryption)" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: pip install -e ".[dev]" || pip install -e . + - run: pip install pytest + - run: pytest tests/test_sqlitedb.py -q -p no:ovoscope + + test-cipher: + name: "SQLite + SQLCipher" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install sqlcipher system library + run: | + sudo apt-get update + sudo apt-get install -y libsqlcipher-dev + - run: pip install -e ".[cipher]" + - run: pip install pytest + - run: pytest tests/test_sqlitedb.py -q -p no:ovoscope diff --git a/README.md b/README.md index d8a0498..3a420b5 100644 --- a/README.md +++ b/README.md @@ -1 +1,76 @@ -# HiveMind SQLite Database \ No newline at end of file +# HiveMind SQLite Database + +SQLite database plugin for [hivemind-core](https://github.com/JarbasHiveMind/HiveMind-core). + +Implements the `AbstractDB` interface via `hivemind-plugin-manager` and stores HiveMind +client records (API keys, crypto keys, access-control lists) in a local SQLite file. + +## Installation + +```bash +pip install hivemind-sqlite-database +``` + +### With encryption support (SQLCipher) + +```bash +# 1. Install the SQLCipher system library +# Debian/Ubuntu: +sudo apt install libsqlcipher0 + +# 2. Install the Python binding via the optional extra +pip install "hivemind-sqlite-database[cipher]" +``` + +> The `sqlcipher3` wheel on PyPI ships its own libsqlcipher for x86_64 Linux, so the +> `apt` step may be optional on that platform. On ARM or Alpine you must build from +> source and will need the system library. + +## Usage + +### Plain (unencrypted) database — default + +```python +from hivemind_sqlite_database import SQLiteDB + +db = SQLiteDB() # stores data in XDG_DATA_HOME/hivemind-core/clients.db +``` + +### Encrypted database (SQLCipher / AES-256) + +```python +from hivemind_sqlite_database import SQLiteDB + +db = SQLiteDB(password="your-strong-passphrase") +``` + +Pass the same `password` every time you open the database. The encryption is +transparent — all existing methods (`add_item`, `search_by_value`, etc.) work +identically. + +> **Data-loss warning**: There is no password recovery. If you lose the passphrase +> the database is permanently unrecoverable. Back up your passphrase securely. + +### hivemind-core configuration + +```json +{ + "database": { + "module": "hivemind-sqlite-db-plugin", + "hivemind-sqlite-db-plugin": { + "name": "clients", + "subfolder": "hivemind-core", + "password": "your-strong-passphrase" + } + } +} +``` + +Leave `"password"` out (or set it to `null`) to use an unencrypted database. + +## Notes + +- An encrypted database cannot be opened by the plain `sqlite3` CLI or stdlib module. +- A plaintext database cannot be opened as encrypted. There is no automatic migration. +- The `password` field maps directly to SQLCipher's `PRAGMA key`. +- WAL journal mode is enabled for both encrypted and unencrypted databases. diff --git a/hivemind_sqlite_database/__init__.py b/hivemind_sqlite_database/__init__.py index 2700b90..c220fec 100644 --- a/hivemind_sqlite_database/__init__.py +++ b/hivemind_sqlite_database/__init__.py @@ -2,7 +2,7 @@ import os.path import sqlite3 import threading -from typing import List, Union, Iterable +from typing import List, Optional, Union, Iterable from ovos_utils.log import LOG from ovos_utils.xdg_utils import xdg_data_home @@ -24,17 +24,43 @@ class SQLiteDB(AbstractDB): """Database implementation using SQLite.""" name: str = "clients" subfolder: str = "hivemind-core" + password: Optional[str] = None def __post_init__(self): """ Initialize the SQLiteDB connection. + + When *password* is set the database is opened via ``sqlcipher3`` and + encrypted with AES-256 (SQLCipher). The system library + ``libsqlcipher0`` must be installed and ``sqlcipher3`` must be + available (``pip install hivemind-sqlite-database[cipher]``). + + When *password* is ``None`` (default) the standard ``sqlite3`` module + is used and the database file is unencrypted. """ db_path = os.path.join(xdg_data_home(), self.subfolder, self.name + ".db") LOG.debug(f"sqlite database path: {db_path}") os.makedirs(os.path.dirname(db_path), exist_ok=True) - self.conn = sqlite3.connect(db_path, check_same_thread=False) - self.conn.row_factory = sqlite3.Row + if self.password is not None: + if self.password == "": + raise ValueError("password must be non-empty when encryption is enabled") + try: + import sqlcipher3 as _sqlcipher + except ImportError: + raise ImportError( + "sqlcipher3 is required to open an encrypted SQLite database. " + "Install the system library (e.g. 'apt install libsqlcipher-dev') " + "then: pip install hivemind-sqlite-database[cipher]" + ) + self.conn = _sqlcipher.connect(db_path, check_same_thread=False) + self.conn.row_factory = _sqlcipher.Row + escaped_password = self.password.replace("'", "''") + self.conn.execute(f"PRAGMA key='{escaped_password}'") + else: + self.conn = sqlite3.connect(db_path, check_same_thread=False) + self.conn.row_factory = sqlite3.Row + self.conn.execute("PRAGMA journal_mode=WAL") self._write_lock = threading.Lock() self._initialize_database() diff --git a/pyproject.toml b/pyproject.toml index 1833c0a..f10c6b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,9 @@ dependencies = [ ] +[project.optional-dependencies] +cipher = ["sqlcipher3"] + [project.urls] Homepage = "https://github.com/JarbasHiveMind/hivemind-sqlite-database" diff --git a/tests/test_sqlitedb.py b/tests/test_sqlitedb.py index 1b414bd..acda6e3 100644 --- a/tests/test_sqlitedb.py +++ b/tests/test_sqlitedb.py @@ -302,5 +302,92 @@ def test_replace_item(self): self.assertEqual(db.search_by_value("api_key", "revoked")[0].client_id, 1) +try: + import sqlcipher3 as _sqlcipher3 # noqa: F401 + _SQLCIPHER_AVAILABLE = True +except ImportError: + _SQLCIPHER_AVAILABLE = False + + +@unittest.skipUnless(_SQLCIPHER_AVAILABLE, "sqlcipher3 not installed") +class TestSQLiteDBEncrypted(unittest.TestCase): + """Tests for the SQLCipher-encrypted path. Skipped when sqlcipher3 is absent.""" + + def _make_encrypted_db(self, path: str, password: str = "hunter2") -> SQLiteDB: + import unittest.mock as mock + db = SQLiteDB.__new__(SQLiteDB) + db.name = os.path.splitext(os.path.basename(path))[0] + db.subfolder = "" + db.password = password + with mock.patch( + "hivemind_sqlite_database.xdg_data_home", + return_value=os.path.dirname(path), + ): + db.__post_init__() + return db + + def test_encrypted_file_unreadable_by_stdlib_sqlite3(self): + """A file created with a password must be opaque to plain sqlite3.""" + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: + path = f.name + os.unlink(path) + try: + db = self._make_encrypted_db(path, password="secret123") + db.add_item(make_client(1, "enc-key")) + db.commit() + # stdlib sqlite3 should not be able to read it + plain_conn = sqlite3.connect(path) + with self.assertRaises(sqlite3.DatabaseError): + plain_conn.execute("SELECT * FROM clients").fetchall() + plain_conn.close() + finally: + if os.path.exists(path): + os.unlink(path) + + def test_encrypted_round_trip(self): + """add_item then search_by_value works through the encryption layer.""" + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: + path = f.name + os.unlink(path) + try: + db = self._make_encrypted_db(path, password="roundtrip") + db.add_item(make_client(1, "enc-api-key", name="alice")) + db.commit() + # Reopen with same password + db2 = self._make_encrypted_db(path, password="roundtrip") + results = db2.search_by_value("api_key", "enc-api-key") + self.assertEqual(len(results), 1) + self.assertEqual(results[0].name, "alice") + finally: + if os.path.exists(path): + os.unlink(path) + + def test_sqlitedb_password_kwarg_raises_importerror_without_sqlcipher3(self): + """Confirmed separately in test_sqlitedb_no_sqlcipher.py; skip here.""" + pass + + +class TestSQLiteDBMissingCipher(unittest.TestCase): + """Verify ImportError is raised when sqlcipher3 is absent and password is given.""" + + def test_importerror_when_sqlcipher3_missing(self): + import sys + import unittest.mock as mock + + # Simulate sqlcipher3 not being installed + with mock.patch.dict(sys.modules, {"sqlcipher3": None}): + with tempfile.TemporaryDirectory() as tmpdir: + with self.assertRaises(ImportError) as ctx: + db = SQLiteDB.__new__(SQLiteDB) + db.name = "test" + db.subfolder = tmpdir + db.password = "secret" + # Patch xdg_data_home so db_path resolves inside tmpdir + with mock.patch("hivemind_sqlite_database.xdg_data_home", + return_value=tmpdir): + db.__post_init__() + self.assertIn("sqlcipher3", str(ctx.exception)) + + if __name__ == "__main__": unittest.main() From eabc7d82324933357e46ea9bf5cdabdad3c786b7 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:20:57 +0000 Subject: [PATCH 03/13] Increment Version to 0.3.0a1 --- hivemind_sqlite_database/version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hivemind_sqlite_database/version.py b/hivemind_sqlite_database/version.py index 0343953..59ee186 100644 --- a/hivemind_sqlite_database/version.py +++ b/hivemind_sqlite_database/version.py @@ -1,8 +1,8 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 2 -VERSION_BUILD = 1 -VERSION_ALPHA = 0 +VERSION_MINOR = 3 +VERSION_BUILD = 0 +VERSION_ALPHA = 1 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From 8e3402fd07b916cdfe0524efc7f61e948f5fbe1b Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:21:40 +0000 Subject: [PATCH 04/13] Update Changelog --- CHANGELOG.md | 51 +++------------------------------------------------ 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d004c0..c520e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,57 +1,12 @@ # Changelog -## [0.2.1a4](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.2.1a4) (2026-04-15) +## [0.3.0a1](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.3.0a1) (2026-04-15) -[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.2.1a3...0.2.1a4) +[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.2.1...0.3.0a1) **Merged pull requests:** -- ci: sync publish workflows from shared templates [\#22](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/22) ([JarbasAl](https://github.com/JarbasAl)) - -## [0.2.1a3](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.2.1a3) (2026-04-15) - -[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.2.1a2...0.2.1a3) - -## [0.2.1a2](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.2.1a2) (2026-04-15) - -[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.2.1a1...0.2.1a2) - -**Merged pull requests:** - -- Update actions/setup-python action to v6 [\#15](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/15) ([renovate[bot]](https://github.com/apps/renovate)) -- Update actions/checkout action to v6 [\#9](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/9) ([renovate[bot]](https://github.com/apps/renovate)) - -## [0.2.1a1](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.2.1a1) (2026-04-15) - -[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.2.0a1...0.2.1a1) - -**Merged pull requests:** - -- fix: move pytest\_plugins to top-level conftest for pytest 9 compatibility [\#18](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/18) ([JarbasAl](https://github.com/JarbasAl)) - -## [0.2.0a1](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.2.0a1) (2026-04-15) - -[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.0.4a2...0.2.0a1) - -**Merged pull requests:** - -- feat: release 0.1.0 — tests, thread-safety, SQL injection fix [\#14](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/14) ([JarbasAl](https://github.com/JarbasAl)) - -## [0.0.4a2](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.0.4a2) (2025-12-19) - -[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.0.4a1...0.0.4a2) - -**Merged pull requests:** - -- chore\(deps\): update actions/setup-python action to v6 [\#12](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/12) ([renovate[bot]](https://github.com/apps/renovate)) - -## [0.0.4a1](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.0.4a1) (2025-12-18) - -[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.0.3...0.0.4a1) - -**Merged pull requests:** - -- chore: Configure Renovate [\#7](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/7) ([renovate[bot]](https://github.com/apps/renovate)) +- feat: add SQLCipher encryption support \(password kwarg\) [\#24](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/24) ([JarbasAl](https://github.com/JarbasAl)) From bbeb6d4e2be2e7fa624b795e0cbbcadf2b1550e1 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 18 May 2026 23:12:54 +0100 Subject: [PATCH 05/13] Preserve client metadata (supersedes #29) (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Preserve client metadata * refactor: require hivemind-plugin-manager>=0.5.0 and drop feature detection hivemind-plugin-manager 0.5.0 ships Client.metadata, so the CLIENT_SUPPORTS_METADATA runtime detection and dual code paths are dead code. Bump the dependency floor and always read/write metadata. The schema migration (ALTER TABLE ... ADD COLUMN metadata) is kept for backwards compatibility with DB files created by older versions. Tests: drop feature-detection skips, add nested-dict and non-ASCII metadata round-trip coverage. * test: add metadata helper and update-semantics coverage - empty-dict default when no metadata kwarg - INSERT OR REPLACE overwrites metadata on same client_id - _metadata_to_json returns '{}' for non-dict inputs - _metadata_from_row coerces NULL / malformed JSON / non-object JSON to {} * docs: clarify _metadata_to_json/_metadata_from_row contracts - _metadata_to_json: document that default=str makes datetime/UUID insertable but they come back as strings (column is opaque JSON, not a typed map) - _metadata_from_row: document the swallow-garbage rationale (one bad row mustn't break iteration) No behavior change. --------- Co-authored-by: Gaëtan Trellu --- .github/workflows/build-tests.yml | 1 + .github/workflows/coverage.yml | 1 + .github/workflows/license_check.yml | 2 + .github/workflows/lint.yml | 1 + .github/workflows/pip_audit.yml | 2 + .github/workflows/release-preview.yml | 1 + .github/workflows/repo-health.yml | 1 + hivemind_sqlite_database/__init__.py | 89 ++++++++++++---- pyproject.toml | 2 +- tests/test_sqlitedb.py | 145 +++++++++++++++++++++++++- 10 files changed, 221 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml index 5c7e53f..966c5d6 100644 --- a/.github/workflows/build-tests.yml +++ b/.github/workflows/build-tests.yml @@ -13,3 +13,4 @@ jobs: python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' install_extras: '' test_path: 'tests' + pr_comment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1b0b9e3..9c3e05a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -15,3 +15,4 @@ jobs: test_path: 'tests/' install_extras: '' min_coverage: 0 + pr_comment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml index 8757eee..185e09c 100644 --- a/.github/workflows/license_check.yml +++ b/.github/workflows/license_check.yml @@ -9,3 +9,5 @@ jobs: license_check: uses: OpenVoiceOS/gh-automations/.github/workflows/license-check.yml@dev secrets: inherit + with: + pr_comment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9a6b7a5..331fdef 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,3 +12,4 @@ jobs: with: ruff: true pre_commit: false # set true if .pre-commit-config.yaml exists + pr_comment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/pip_audit.yml b/.github/workflows/pip_audit.yml index bb3ca4d..cf9ef22 100644 --- a/.github/workflows/pip_audit.yml +++ b/.github/workflows/pip_audit.yml @@ -9,3 +9,5 @@ jobs: pip_audit: uses: OpenVoiceOS/gh-automations/.github/workflows/pip-audit.yml@dev secrets: inherit + with: + pr_comment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/release-preview.yml b/.github/workflows/release-preview.yml index 3404ca7..9b5b5d7 100644 --- a/.github/workflows/release-preview.yml +++ b/.github/workflows/release-preview.yml @@ -7,6 +7,7 @@ on: jobs: release_preview: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: OpenVoiceOS/gh-automations/.github/workflows/release-preview.yml@dev secrets: inherit with: diff --git a/.github/workflows/repo-health.yml b/.github/workflows/repo-health.yml index 96f690d..7487046 100644 --- a/.github/workflows/repo-health.yml +++ b/.github/workflows/repo-health.yml @@ -7,6 +7,7 @@ on: jobs: repo_health: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: OpenVoiceOS/gh-automations/.github/workflows/repo-health.yml@dev secrets: inherit with: diff --git a/hivemind_sqlite_database/__init__.py b/hivemind_sqlite_database/__init__.py index c220fec..9642b21 100644 --- a/hivemind_sqlite_database/__init__.py +++ b/hivemind_sqlite_database/__init__.py @@ -11,11 +11,12 @@ from dataclasses import dataclass + _VALID_COLUMNS = frozenset({ "client_id", "api_key", "name", "description", "is_admin", "last_seen", "intent_blacklist", "skill_blacklist", "message_blacklist", "allowed_types", "crypto_key", "password", - "can_broadcast", "can_escalate", "can_propagate", + "can_broadcast", "can_escalate", "can_propagate", "metadata", }) @@ -86,9 +87,16 @@ def _initialize_database(self): password TEXT, can_broadcast BOOLEAN DEFAULT TRUE, can_escalate BOOLEAN DEFAULT TRUE, - can_propagate BOOLEAN DEFAULT TRUE + can_propagate BOOLEAN DEFAULT TRUE, + metadata TEXT ) """) + columns = { + row["name"] + for row in self.conn.execute("PRAGMA table_info(clients)").fetchall() + } + if "metadata" not in columns: + self.conn.execute("ALTER TABLE clients ADD COLUMN metadata TEXT") def add_item(self, client: Client) -> bool: """ @@ -101,14 +109,15 @@ def add_item(self, client: Client) -> bool: True if the addition was successful, False otherwise. """ try: + metadata_json = self._metadata_to_json(client.metadata or {}) with self._write_lock, self.conn: self.conn.execute(""" INSERT OR REPLACE INTO clients ( client_id, api_key, name, description, is_admin, last_seen, intent_blacklist, skill_blacklist, message_blacklist, allowed_types, crypto_key, password, - can_broadcast, can_escalate, can_propagate - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + can_broadcast, can_escalate, can_propagate, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( client.client_id, client.api_key, client.name, client.description, client.is_admin, client.last_seen, @@ -117,7 +126,8 @@ def add_item(self, client: Client) -> bool: json.dumps(client.message_blacklist), json.dumps(client.allowed_types), client.crypto_key, client.password, - client.can_broadcast, client.can_escalate, client.can_propagate + client.can_broadcast, client.can_escalate, client.can_propagate, + metadata_json, )) return True except sqlite3.Error as e: @@ -180,20 +190,55 @@ def commit(self) -> bool: @staticmethod def _row_to_client(row: sqlite3.Row) -> Client: """Convert a database row to a Client instance.""" - return Client( - client_id=int(row["client_id"]), - api_key=row["api_key"], - name=row["name"], - description=row["description"], - is_admin=bool(row["is_admin"]), - last_seen=row["last_seen"], - intent_blacklist=json.loads(row["intent_blacklist"] or "[]"), - skill_blacklist=json.loads(row["skill_blacklist"] or "[]"), - message_blacklist=json.loads(row["message_blacklist"] or "[]"), - allowed_types=json.loads(row["allowed_types"] or "[]"), - crypto_key=row["crypto_key"], - password=row["password"], - can_broadcast=bool(row["can_broadcast"]), - can_escalate=bool(row["can_escalate"]), - can_propagate=bool(row["can_propagate"]) - ) + kwargs = { + "client_id": int(row["client_id"]), + "api_key": row["api_key"], + "name": row["name"], + "description": row["description"], + "is_admin": bool(row["is_admin"]), + "last_seen": row["last_seen"], + "intent_blacklist": json.loads(row["intent_blacklist"] or "[]"), + "skill_blacklist": json.loads(row["skill_blacklist"] or "[]"), + "message_blacklist": json.loads(row["message_blacklist"] or "[]"), + "allowed_types": json.loads(row["allowed_types"] or "[]"), + "crypto_key": row["crypto_key"], + "password": row["password"], + "can_broadcast": bool(row["can_broadcast"]), + "can_escalate": bool(row["can_escalate"]), + "can_propagate": bool(row["can_propagate"]), + "metadata": SQLiteDB._metadata_from_row(row), + } + return Client(**kwargs) + + @staticmethod + def _metadata_to_json(metadata: object) -> str: + """Serialize ``Client.metadata`` for storage in the ``metadata`` column. + + ``metadata`` is documented as a free-form dict; we use ``default=str`` + so callers can stash convenience values like ``datetime`` or ``UUID`` + without crashing on insert. Note that these come back as strings on + read — the column is opaque JSON, not a typed map. Returns ``"{}"`` + for non-dict input and on any (unexpected) serialisation failure + rather than corrupting the row. + """ + if not isinstance(metadata, dict): + return "{}" + try: + return json.dumps(metadata, default=str) + except (TypeError, ValueError): + return "{}" + + @staticmethod + def _metadata_from_row(row: sqlite3.Row) -> dict: + """Decode the ``metadata`` column into a dict, swallowing garbage. + + Returns ``{}`` for NULL, malformed JSON, or valid-JSON-that-isn't-an-object. + Lets a single bad row not poison iteration over the table. + """ + if "metadata" not in row.keys() or not row["metadata"]: + return {} + try: + metadata = json.loads(row["metadata"]) + except (TypeError, ValueError): + return {} + return metadata if isinstance(metadata, dict) else {} diff --git a/pyproject.toml b/pyproject.toml index f10c6b0..db99161 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ license = { text = "Apache-2.0" } authors = [{ name = "jarbasAi", email = "jarbasai@mailfence.com" }] requires-python = ">=3.9" dependencies = [ - "hivemind-plugin-manager>=0.1.0,<1.0.0", + "hivemind-plugin-manager>=0.5.0,<1.0.0", "hivemind-bus-client", "ovos-utils", ] diff --git a/tests/test_sqlitedb.py b/tests/test_sqlitedb.py index acda6e3..9477283 100644 --- a/tests/test_sqlitedb.py +++ b/tests/test_sqlitedb.py @@ -236,9 +236,150 @@ def test_boolean_fields_remain_bool_after_round_trip(self): self.assertIs(type(results[0].can_broadcast), bool) self.assertFalse(results[0].can_broadcast) + def test_metadata_survives_round_trip(self): + db = make_db() + db.add_item( + make_client( + 1, + "k1", + metadata={"owner_id": "owner-123"}, + ) + ) + + results = db.search_by_value("api_key", "k1") + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].metadata, {"owner_id": "owner-123"}) + + def test_metadata_can_be_searched(self): + db = make_db() + db.add_item( + make_client( + 1, + "k1", + metadata={"owner_id": "owner-123"}, + ) + ) + + results = db.search_by_value("metadata", '{"owner_id": "owner-123"}') + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].api_key, "k1") + + def test_initialize_database_migrates_legacy_clients_table(self): + db = object.__new__(SQLiteDB) + db.name = "clients" + db.subfolder = "hivemind-core" + db.conn = sqlite3.connect(":memory:", check_same_thread=False) + db.conn.row_factory = sqlite3.Row + db._write_lock = threading.Lock() + with db.conn: + db.conn.execute(""" + CREATE TABLE clients ( + client_id INTEGER PRIMARY KEY, + api_key VARCHAR(255) NOT NULL, + name VARCHAR(255), + description VARCHAR(255), + is_admin BOOLEAN DEFAULT FALSE, + last_seen REAL DEFAULT -1, + intent_blacklist TEXT, + skill_blacklist TEXT, + message_blacklist TEXT, + allowed_types TEXT, + crypto_key VARCHAR(16), + password TEXT, + can_broadcast BOOLEAN DEFAULT TRUE, + can_escalate BOOLEAN DEFAULT TRUE, + can_propagate BOOLEAN DEFAULT TRUE + ) + """) + db.conn.execute("INSERT INTO clients (client_id, api_key) VALUES (1, 'k1')") + + db._initialize_database() + + columns = { + row["name"] + for row in db.conn.execute("PRAGMA table_info(clients)").fetchall() + } + self.assertIn("metadata", columns) + client = db.search_by_value("api_key", "k1")[0] + self.assertEqual(client.metadata, {}) + + def test_metadata_nested_dict_round_trip(self): + db = make_db() + meta = { + "owner": {"id": "owner-1", "tags": ["a", "b"]}, + "counts": {"x": 1, "y": 2}, + } + db.add_item(make_client(1, "k1", metadata=meta)) + results = db.search_by_value("api_key", "k1") + self.assertEqual(len(results), 1) + self.assertEqual(results[0].metadata, meta) + + def test_metadata_non_ascii_round_trip(self): + db = make_db() + meta = {"name": "Zé Ninguém", "emoji": "🚀", "ru": "Привет"} + db.add_item(make_client(1, "k1", metadata=meta)) + results = db.search_by_value("api_key", "k1") + self.assertEqual(len(results), 1) + self.assertEqual(results[0].metadata, meta) + + def test_metadata_survives_iteration(self): + db = make_db() + db.add_item( + make_client( + 1, + "k1", + metadata={"owner_id": "owner-123"}, + ) + ) + + clients = list(db) + + self.assertEqual(len(clients), 1) + self.assertEqual(clients[0].metadata, {"owner_id": "owner-123"}) + + def test_metadata_defaults_to_empty_dict_when_not_provided(self): + db = make_db() + db.add_item(make_client(1, "k1")) + results = db.search_by_value("api_key", "k1") + self.assertEqual(results[0].metadata, {}) + + def test_metadata_overwritten_on_reinsert_with_same_client_id(self): + db = make_db() + db.add_item(make_client(1, "k1", metadata={"v": 1})) + db.add_item(make_client(1, "k1", metadata={"v": 2, "extra": "x"})) + results = db.search_by_value("api_key", "k1") + self.assertEqual(len(results), 1) + self.assertEqual(results[0].metadata, {"v": 2, "extra": "x"}) + + def test_metadata_to_json_returns_empty_for_non_dict(self): + self.assertEqual(SQLiteDB._metadata_to_json("not a dict"), "{}") + self.assertEqual(SQLiteDB._metadata_to_json(None), "{}") + self.assertEqual(SQLiteDB._metadata_to_json(42), "{}") + + def test_metadata_from_row_returns_empty_for_garbage_or_missing(self): + db = make_db() + # legacy-style row with explicit NULL metadata + db.add_item(make_client(1, "k1")) + with db.conn: + db.conn.execute("UPDATE clients SET metadata = NULL WHERE client_id = 1") + results = db.search_by_value("api_key", "k1") + self.assertEqual(results[0].metadata, {}) + # garbage JSON in the metadata column → coerce to {} + with db.conn: + db.conn.execute("UPDATE clients SET metadata = 'not json{' WHERE client_id = 1") + results = db.search_by_value("api_key", "k1") + self.assertEqual(results[0].metadata, {}) + # valid JSON but not an object → coerce to {} + with db.conn: + db.conn.execute("UPDATE clients SET metadata = '[1,2,3]' WHERE client_id = 1") + results = db.search_by_value("api_key", "k1") + self.assertEqual(results[0].metadata, {}) + def test_full_client_fields_preserved(self): db = make_db() - c = Client( + c = make_client( client_id=42, api_key="full-key", name="test-client", @@ -254,6 +395,7 @@ def test_full_client_fields_preserved(self): can_broadcast=True, can_escalate=False, can_propagate=True, + metadata={"owner_id": "owner-123"}, ) db.add_item(c) results = db.search_by_value("api_key", "full-key") @@ -267,6 +409,7 @@ def test_full_client_fields_preserved(self): self.assertEqual(r.crypto_key, "1234567890123456") self.assertEqual(r.password, "secret") self.assertFalse(r.can_escalate) + self.assertEqual(r.metadata, {"owner_id": "owner-123"}) class TestSQLiteDBCommit(unittest.TestCase): From 46cd8b55485f19795c683ba5a301223cd7ef9be5 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 18 May 2026 22:13:07 +0000 Subject: [PATCH 06/13] Increment Version to 0.3.0a2 --- hivemind_sqlite_database/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hivemind_sqlite_database/version.py b/hivemind_sqlite_database/version.py index 59ee186..af8c8b5 100644 --- a/hivemind_sqlite_database/version.py +++ b/hivemind_sqlite_database/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 0 VERSION_MINOR = 3 VERSION_BUILD = 0 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From 228aad059d6af56ebccc5339ca8fdf4e3286a7f5 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Mon, 18 May 2026 22:13:31 +0000 Subject: [PATCH 07/13] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c520e0b..3c659e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.3.0a2](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.3.0a2) (2026-05-18) + +[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.3.0a1...0.3.0a2) + +**Closed issues:** + +- security: encrypted db [\#2](https://github.com/JarbasHiveMind/hivemind-sqlite-database/issues/2) + +**Merged pull requests:** + +- Preserve client metadata \(supersedes \#29\) [\#30](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/30) ([JarbasAl](https://github.com/JarbasAl)) + ## [0.3.0a1](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.3.0a1) (2026-04-15) [Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.2.1...0.3.0a1) From 9f578ce40b63ea152f2d6ef184bf249c7fde4fb2 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 20 May 2026 18:03:43 +0100 Subject: [PATCH 08/13] ci: pass PYPI_TOKEN explicitly, drop secrets:inherit elsewhere (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: pass PYPI_TOKEN explicitly, drop secrets:inherit elsewhere secrets:inherit fails for the publish workflows (publish-stable, publish-alpha) — pass PYPI_TOKEN explicitly. For workflows that don't declare any secrets upstream, drop the secrets: line entirely. * ci: add workflow_dispatch to release_workflow so alphas can be triggered manually Other repos in the family (hivemind-plugin-manager, hivemind-ovos-agent-plugin, HiveMind-core, hivemind-redis-database) all have workflow_dispatch on this workflow. sqlite was missing it, blocking manual alpha-release triggering. --- .github/workflows/build-tests.yml | 1 - .github/workflows/coverage.yml | 1 - .github/workflows/license_check.yml | 1 - .github/workflows/lint.yml | 1 - .github/workflows/pip_audit.yml | 1 - .github/workflows/publish_stable.yml | 3 ++- .github/workflows/release-preview.yml | 1 - .github/workflows/release_workflow.yml | 4 +++- .github/workflows/repo-health.yml | 1 - 9 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml index 966c5d6..b04f41e 100644 --- a/.github/workflows/build-tests.yml +++ b/.github/workflows/build-tests.yml @@ -8,7 +8,6 @@ on: jobs: build: uses: OpenVoiceOS/gh-automations/.github/workflows/build-tests.yml@dev - secrets: inherit with: python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' install_extras: '' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9c3e05a..cfb943f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -8,7 +8,6 @@ on: jobs: coverage: uses: OpenVoiceOS/gh-automations/.github/workflows/coverage.yml@dev - secrets: inherit with: python_version: '3.11' coverage_source: 'hivemind_sqlite_database' diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml index 185e09c..cb7cfb5 100644 --- a/.github/workflows/license_check.yml +++ b/.github/workflows/license_check.yml @@ -8,6 +8,5 @@ on: jobs: license_check: uses: OpenVoiceOS/gh-automations/.github/workflows/license-check.yml@dev - secrets: inherit with: pr_comment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 331fdef..c8000e6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,6 @@ on: jobs: lint: uses: OpenVoiceOS/gh-automations/.github/workflows/lint.yml@dev - secrets: inherit with: ruff: true pre_commit: false # set true if .pre-commit-config.yaml exists diff --git a/.github/workflows/pip_audit.yml b/.github/workflows/pip_audit.yml index cf9ef22..d44ece3 100644 --- a/.github/workflows/pip_audit.yml +++ b/.github/workflows/pip_audit.yml @@ -8,6 +8,5 @@ on: jobs: pip_audit: uses: OpenVoiceOS/gh-automations/.github/workflows/pip-audit.yml@dev - secrets: inherit with: pr_comment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/.github/workflows/publish_stable.yml b/.github/workflows/publish_stable.yml index 9ab5b7a..a452be6 100644 --- a/.github/workflows/publish_stable.yml +++ b/.github/workflows/publish_stable.yml @@ -7,7 +7,8 @@ on: jobs: publish_stable: uses: OpenVoiceOS/gh-automations/.github/workflows/publish-stable.yml@dev - secrets: inherit + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} with: branch: 'master' version_file: 'hivemind_sqlite_database/version.py' diff --git a/.github/workflows/release-preview.yml b/.github/workflows/release-preview.yml index 9b5b5d7..527970a 100644 --- a/.github/workflows/release-preview.yml +++ b/.github/workflows/release-preview.yml @@ -9,7 +9,6 @@ jobs: release_preview: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: OpenVoiceOS/gh-automations/.github/workflows/release-preview.yml@dev - secrets: inherit with: package_name: 'hivemind_sqlite_database' version_file: 'hivemind_sqlite_database/version.py' diff --git a/.github/workflows/release_workflow.yml b/.github/workflows/release_workflow.yml index 93dfa8c..b1861c3 100644 --- a/.github/workflows/release_workflow.yml +++ b/.github/workflows/release_workflow.yml @@ -4,11 +4,13 @@ on: pull_request: types: [closed] branches: [dev] + workflow_dispatch: jobs: publish_alpha: uses: OpenVoiceOS/gh-automations/.github/workflows/publish-alpha.yml@dev - secrets: inherit + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} with: branch: 'dev' base_branch: 'master' diff --git a/.github/workflows/repo-health.yml b/.github/workflows/repo-health.yml index 7487046..0858e05 100644 --- a/.github/workflows/repo-health.yml +++ b/.github/workflows/repo-health.yml @@ -9,6 +9,5 @@ jobs: repo_health: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: OpenVoiceOS/gh-automations/.github/workflows/repo-health.yml@dev - secrets: inherit with: version_file: 'hivemind_sqlite_database/version.py' From 90507d11e8db9a12594ad7d9c05e4f79c82a4b5f Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 20 May 2026 17:05:25 +0000 Subject: [PATCH 09/13] Increment Version to 0.3.0a3 --- hivemind_sqlite_database/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hivemind_sqlite_database/version.py b/hivemind_sqlite_database/version.py index af8c8b5..b44a217 100644 --- a/hivemind_sqlite_database/version.py +++ b/hivemind_sqlite_database/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 0 VERSION_MINOR = 3 VERSION_BUILD = 0 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From 38b083642258ffcbf923552dd89183b7a7067424 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 20 May 2026 17:06:01 +0000 Subject: [PATCH 10/13] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c659e8..e7cfa41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.3.0a3](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.3.0a3) (2026-05-20) + +[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.3.0a2...0.3.0a3) + +**Merged pull requests:** + +- ci: pass PYPI\_TOKEN explicitly, drop secrets:inherit elsewhere [\#33](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/33) ([JarbasAl](https://github.com/JarbasAl)) + ## [0.3.0a2](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.3.0a2) (2026-05-18) [Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.3.0a1...0.3.0a2) From 1c28bfa14b197e56c2a13dec47e24dd3b1bf1417 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:52:29 +0100 Subject: [PATCH 11/13] =?UTF-8?q?ci:=20dedupe=20tests.yml=20=E2=80=94=20dr?= =?UTF-8?q?op=20test-plain,=20rename=20to=20cipher-tests.yml=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous tests.yml had two jobs: - test-plain: redundant with the shared build-tests.yml (which already runs the unencrypted path across Python 3.10-3.14). - test-cipher: unique — installs libsqlcipher-dev + the [cipher] extra and exercises the SQLCipher path on every PR. Keep the cipher job, drop the duplicate, rename the file so its purpose is obvious in the Actions UI. Also align the trigger surface with the rest of the workflows (pull_request on the named branches + workflow_dispatch), instead of the catch-all push/PR on every branch. --- .../workflows/{tests.yml => cipher-tests.yml} | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) rename .github/workflows/{tests.yml => cipher-tests.yml} (54%) diff --git a/.github/workflows/tests.yml b/.github/workflows/cipher-tests.yml similarity index 54% rename from .github/workflows/tests.yml rename to .github/workflows/cipher-tests.yml index 3a7e54b..8dea296 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/cipher-tests.yml @@ -1,24 +1,16 @@ -name: Tests +name: Cipher Tests + +# Dedicated workflow for the [cipher] extra (sqlcipher3 + libsqlcipher-dev). +# The shared build-tests.yml covers the unencrypted path on Python 3.10-3.14; +# this one adds a single Python 3.11 run with sqlcipher installed so the +# encrypted backend is exercised on every PR. on: - push: - branches: ["**"] pull_request: - branches: ["**"] + branches: [dev, master, main] + workflow_dispatch: jobs: - test-plain: - name: "SQLite (no encryption)" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - run: pip install -e ".[dev]" || pip install -e . - - run: pip install pytest - - run: pytest tests/test_sqlitedb.py -q -p no:ovoscope - test-cipher: name: "SQLite + SQLCipher" runs-on: ubuntu-latest From 173f0b1ce2978ff0dde3c63181f48c7549ddcf98 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:52:41 +0000 Subject: [PATCH 12/13] Increment Version to 0.3.0a4 --- hivemind_sqlite_database/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hivemind_sqlite_database/version.py b/hivemind_sqlite_database/version.py index b44a217..3fec4d7 100644 --- a/hivemind_sqlite_database/version.py +++ b/hivemind_sqlite_database/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 0 VERSION_MINOR = 3 VERSION_BUILD = 0 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "") From 588f9ec76062ee9b74a64da636f715b7ba2bcb23 Mon Sep 17 00:00:00 2001 From: JarbasAl <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:52:59 +0000 Subject: [PATCH 13/13] Update Changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7cfa41..6e5970f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.3.0a4](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.3.0a4) (2026-06-05) + +[Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.3.0a3...0.3.0a4) + +**Merged pull requests:** + +- ci: dedupe tests.yml — drop test-plain, rename to cipher-tests.yml [\#34](https://github.com/JarbasHiveMind/hivemind-sqlite-database/pull/34) ([JarbasAl](https://github.com/JarbasAl)) + ## [0.3.0a3](https://github.com/JarbasHiveMind/hivemind-sqlite-database/tree/0.3.0a3) (2026-05-20) [Full Changelog](https://github.com/JarbasHiveMind/hivemind-sqlite-database/compare/0.3.0a2...0.3.0a3)