rename: the server is Inkwell — package, env vars, image, compose, export marker
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 45s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m33s
Desktop (Tauri) / Update manifest (push) Successful in 7s
Android / Kotlin + Rust (APK) (push) Successful in 11m25s

Step 2 of milestone 481. The operator chose a full rename (Scribe note 5071), so
this goes past the display strings into the identities:

- src/thoughtsync → src/inkwell; every import, the Dockerfile and both compose
  commands, alembic env, pyproject
- THOUGHTSYNC_* → INKWELL_* (database URL, secret key, log level, tag/port/bind)
- container data dir /var/thoughtsync → /var/inkwell
- image git.fabledsword.com/bvandeusen/inkwell; Postgres user/db default inkwell;
  CI's integration service follows
- the files the image serves are inkwell.*. fetch-clients.sh still fetches the
  thoughtsync-named release assets, because the lanes that publish them are
  renamed in steps 3 and 4
- exports are written with app "inkwell"

Two deliberate exceptions, both because data rides on them:

- compose volumes are now named explicitly and overridable (INKWELL_DB_VOLUME,
  INKWELL_DATA_VOLUME), so a deployment installed as ThoughtSync points at the
  volumes and DB identity it already has. .env.example says exactly what to set
- import still accepts app "thoughtsync", because exports written before the
  rename are backups. Tested both ways

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 14:18:49 -04:00
co-authored by Claude Opus 5.5
parent f806e35d41
commit a706644455
78 changed files with 231 additions and 168 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
import pytest
from thoughtsync.config import Config
from inkwell.config import Config
@pytest.fixture(autouse=True)
def _isolated_data_dir(tmp_path, monkeypatch):
"""Keep any media writes on an isolated temp dir rather than the fixed
/var/thoughtsync. DB-free unit tests never actually hit it (create_app takes its
/var/inkwell. DB-free unit tests never actually hit it (create_app takes its
signing key from env-or-random, not a file), but this stays defensive."""
monkeypatch.setattr(Config, "DATA_DIR", str(tmp_path / "data"))
yield
+2 -2
View File
@@ -1,7 +1,7 @@
import uuid
from thoughtsync.acl import visible_to_user
from thoughtsync.models.user import User
from inkwell.acl import visible_to_user
from inkwell.models.user import User
def test_visible_to_user_builds_owner_or_shared_predicate():
+1 -1
View File
@@ -1,6 +1,6 @@
import pytest
from thoughtsync.app import create_app
from inkwell.app import create_app
@pytest.fixture
+2 -2
View File
@@ -1,6 +1,6 @@
import pytest
from thoughtsync.app import create_app
from inkwell.app import create_app
@pytest.fixture
@@ -61,7 +61,7 @@ async def test_no_version_in_the_environment_reports_unknown(monkeypatch):
happens to hold, so bumping the packaging version cannot make this pass for the
wrong reason.
"""
from thoughtsync import __version__
from inkwell import __version__
monkeypatch.delenv("APP_VERSION", raising=False)
reported = await reported_version()
+14 -10
View File
@@ -3,9 +3,9 @@ from pathlib import Path
import pytest
from thoughtsync import client_dist
from thoughtsync.app import create_app
from thoughtsync.client_dist import (
from inkwell import client_dist
from inkwell.app import create_app
from inkwell.client_dist import (
APK_NAME,
BY_ID,
MANIFEST_NAME,
@@ -15,7 +15,7 @@ from thoughtsync.client_dist import (
release,
releases,
)
from thoughtsync.config import Config
from inkwell.config import Config
# DB-free, like the rest of this suite — the test lane runs no Postgres. That is
# why the advertisement is asserted through `advertisement()` rather than through
@@ -61,7 +61,7 @@ def coded(platform_id: str, value: int):
def _empty_baked_client(tmp_path, monkeypatch):
"""Point the baked-in copy at an empty directory.
In a source checkout `src/thoughtsync/client/` does not exist, so these tests
In a source checkout `src/inkwell/client/` does not exist, so these tests
would pass anyway — but only by accident of where they are run. A built image
has real clients there, and a test that silently depends on which tree it is in
is one that will eventually lie.
@@ -118,13 +118,17 @@ def test_every_platform_has_its_own_filenames():
assert not set(artifacts) & set(sidecars)
def test_the_android_names_are_the_ones_already_published():
"""Pinned because renaming them is a tidy-up that strands every installed phone.
def test_the_android_names_are_the_ones_the_image_build_writes():
"""Pinned against `packaging/fetch-clients.sh`, which writes the APK and its
sidecar under these names when it bakes them into the image. The two are a pair
with nothing checking them against each other, so a rename on one side reads as
"no Android client" on the other rather than as an error.
The Android lane publishes these exact names and CI bakes them in under them.
Phones never ask for these names; they poll `/api/client/android`. The names
changed once, with the rename to Inkwell (Scribe note 5071).
"""
assert APK_NAME == "thoughtsync.apk"
assert MANIFEST_NAME == "thoughtsync-android.json"
assert APK_NAME == "inkwell.apk"
assert MANIFEST_NAME == "inkwell-android.json"
# --- absence is an ordinary answer -------------------------------------------
+1 -1
View File
@@ -1,6 +1,6 @@
import pytest
from thoughtsync.app import create_app
from inkwell.app import create_app
@pytest.fixture
+17 -17
View File
@@ -22,20 +22,20 @@ import pytest
import pytest_asyncio
from sqlalchemy import select, text
from thoughtsync import ratelimit
from thoughtsync.app import create_app
from thoughtsync.db import dispose_engine, session_scope
from thoughtsync.models.label import NoteLabel
from thoughtsync.models.note import Note
from thoughtsync.models.user import User
from thoughtsync.notes.tags import _lift_and_reconcile_tags
from thoughtsync.settings import get_setting, live, refresh_live, reset_live, set_settings
from thoughtsync.notes.checklist import parse_items, set_item_checked
from thoughtsync.notes.helpers import derive_display_title
from thoughtsync.models.note_link_preview import NoteLinkPreview
from thoughtsync.models.note_revision import NoteRevision
from thoughtsync.revisions import REVISION_WINDOW_MINUTES, should_snapshot
from thoughtsync.unfurl_queue import _unfurl_new_urls, detect_urls
from inkwell import ratelimit
from inkwell.app import create_app
from inkwell.db import dispose_engine, session_scope
from inkwell.models.label import NoteLabel
from inkwell.models.note import Note
from inkwell.models.user import User
from inkwell.notes.tags import _lift_and_reconcile_tags
from inkwell.settings import get_setting, live, refresh_live, reset_live, set_settings
from inkwell.notes.checklist import parse_items, set_item_checked
from inkwell.notes.helpers import derive_display_title
from inkwell.models.note_link_preview import NoteLinkPreview
from inkwell.models.note_revision import NoteRevision
from inkwell.revisions import REVISION_WINDOW_MINUTES, should_snapshot
from inkwell.unfurl_queue import _unfurl_new_urls, detect_urls
pytestmark = pytest.mark.integration
@@ -305,7 +305,7 @@ async def test_auto_unfurl_stores_a_preview_and_skips_what_is_cached(db, owner,
calls.append(url)
return {"url": url, "title": f"T {url}", "description": None, "image_url": None, "site_name": "example.com"}
monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl)
monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl)
await _unfurl_new_urls(note.id, note.body)
assert sorted(calls) == ["https://example.com/a", "https://example.com/b"]
@@ -331,7 +331,7 @@ async def test_auto_unfurl_drops_a_preview_whose_url_left_the_body(db, owner, mo
# Simulate the body changing while the request was in the air.
return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None}
monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl)
monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl)
note.body = "changed my mind"
await db.commit()
@@ -352,7 +352,7 @@ async def test_detection_agrees_with_what_gets_stored(db, owner, monkeypatch):
async def fake_unfurl(url):
return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None}
monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl)
monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl)
await _unfurl_new_urls(note.id, body)
stored = {
+3 -3
View File
@@ -2,9 +2,9 @@ import uuid
import pytest
from thoughtsync.app import create_app
from thoughtsync.models.label import Label
from thoughtsync.serialize import serialize_label
from inkwell.app import create_app
from inkwell.models.label import Label
from inkwell.serialize import serialize_label
@pytest.fixture
+35 -8
View File
@@ -1,12 +1,15 @@
import io
import json
import zipfile
from datetime import datetime, timezone
import pytest
from thoughtsync.app import create_app
from thoughtsync.common import coerce_bool, parse_dt
from thoughtsync.colors import NOTE_COLORS
from thoughtsync.models.note import Note
from thoughtsync.notes.checklist import (
from inkwell.app import create_app
from inkwell.common import coerce_bool, parse_dt
from inkwell.colors import NOTE_COLORS
from inkwell.models.note import Note
from inkwell.notes.checklist import (
append_item,
parse_items,
remove_item,
@@ -14,13 +17,15 @@ from thoughtsync.notes.checklist import (
set_item_text,
strip_marker,
)
from thoughtsync.unfurl_queue import detect_urls
from thoughtsync.notes import (
from inkwell.unfurl_queue import detect_urls
from inkwell.notes import (
_ImportBudget,
_attachment_ext,
_header_filename,
_keep_spec,
_native_spec,
_note_markdown,
_read_import_specs,
_safe_filename,
_slugify,
_usec_to_dt,
@@ -32,7 +37,8 @@ from thoughtsync.notes import (
parse_list_items,
parse_tags,
)
from thoughtsync.notes.tags import split_body_tags
from inkwell.notes.import_export import NATIVE_APP_MARKERS
from inkwell.notes.tags import split_body_tags
@pytest.fixture
@@ -672,3 +678,24 @@ def test_native_spec_of_a_pre_m304_export_still_carries_its_items():
# items at all.
old = {"body": "shopping", "items": [{"text": "milk", "checked": True}]}
assert _native_spec(old)["items"] == [{"text": "milk", "checked": True}]
def _export_zip(app_marker: str) -> zipfile.ZipFile:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("notes.json", json.dumps({"app": app_marker, "notes": [{"body": "kept"}]}))
return zipfile.ZipFile(io.BytesIO(buf.getvalue()))
@pytest.mark.parametrize("marker", ["inkwell", "thoughtsync"])
def test_an_export_imports_under_either_name(marker):
# "thoughtsync" is what every export written before the rename to Inkwell says.
# Those zips are backups, so they have to keep restoring as native exports rather
# than falling through to the Keep importer, which would find nothing in them.
specs, source = _read_import_specs(_export_zip(marker), _ImportBudget())
assert source == "inkwell"
assert [s["body"] for s in specs] == ["kept"]
def test_new_exports_are_written_as_inkwell():
assert NATIVE_APP_MARKERS[0] == "inkwell"
+2 -2
View File
@@ -4,8 +4,8 @@ The whole security property is "a caller cannot forge their own address", and it
on counting in from the RIGHT of the header rather than the left. These are the cases
that tell the two apart — pure functions, no request context, no database.
"""
from thoughtsync.proxy import forwarded_for, trusted_entry
from thoughtsync.settings import live
from inkwell.proxy import forwarded_for, trusted_entry
from inkwell.settings import live
PEER = "10.0.0.1" # the socket address: our own proxy, or the caller when unproxied
+4 -4
View File
@@ -12,10 +12,10 @@ import time
import pytest
from thoughtsync import ratelimit
from thoughtsync.settings import live
from thoughtsync.app import create_app
from thoughtsync.ratelimit import SlidingWindow
from inkwell import ratelimit
from inkwell.settings import live
from inkwell.app import create_app
from inkwell.ratelimit import SlidingWindow
def window(limit: int, window_s: float) -> SlidingWindow:
+2 -2
View File
@@ -2,14 +2,14 @@ from datetime import datetime, timedelta, timezone
import pytest
from thoughtsync.retention import (
from inkwell.retention import (
SWEEP_BATCH,
SWEEP_INTERVAL_SECONDS,
SWEEP_STARTUP_DELAY_SECONDS,
expired_before,
sweep_expired_trash,
)
from thoughtsync.settings import REGISTRY, get_public_config, validate_updates
from inkwell.settings import REGISTRY, get_public_config, validate_updates
NOW = datetime(2026, 7, 26, 12, 0, tzinfo=timezone.utc)
+2 -2
View File
@@ -1,7 +1,7 @@
import pytest
from thoughtsync.app import create_app
from thoughtsync.saved_filters import clean_params
from inkwell.app import create_app
from inkwell.saved_filters import clean_params
@pytest.fixture
+1 -1
View File
@@ -1,4 +1,4 @@
from thoughtsync.security import generate_token, hash_password, hash_token, verify_password
from inkwell.security import generate_token, hash_password, hash_token, verify_password
def test_password_roundtrip():
+1 -1
View File
@@ -6,7 +6,7 @@ hook, and this suite has no Postgres.
"""
import pytest
from thoughtsync.app import create_app
from inkwell.app import create_app
@pytest.fixture
+1 -1
View File
@@ -1,4 +1,4 @@
from thoughtsync.settings import REGISTRY, validate_updates
from inkwell.settings import REGISTRY, validate_updates
def test_registry_has_expected_keys():
+2 -2
View File
@@ -2,8 +2,8 @@ from datetime import datetime, timezone
import pytest
from thoughtsync.app import create_app
from thoughtsync.sync import (
from inkwell.app import create_app
from inkwell.sync import (
DEFAULT_LIMIT,
MAX_LIMIT,
MIN_CLIENT_PROTOCOL_VERSION,
+2 -2
View File
@@ -2,8 +2,8 @@ import ipaddress
import pytest
from thoughtsync.app import create_app
from thoughtsync.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url
from inkwell.app import create_app
from inkwell.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url
@pytest.fixture