Compare commits

..
Author SHA1 Message Date
renovate 768c9594b0 chore(deps): update docker.io/library/python docker tag to v3.15
Docker Build and Push / integration-test (pull_request) Waiting to run
PR Checks / pytest (pull_request) Waiting to run
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / test (pull_request) Waiting to run
PR Checks / hadolint (pull_request) Waiting to run
PR Checks / build-test (pull_request) Waiting to run
2026-10-18 19:27:12 +00:00
jcabillot 2e247141fc Merge pull request 'ci(tag): déclenche Renovate sur tf-depl-kubernetes dès que l'image de release est poussée (mcp-maildir)' (#62) from ci/renovate-dispatch-on-release into main
Main Release / test (push) Successful in 4m57s
Main Release / integration-test (push) Successful in 7m25s
Main Release / build (push) Waiting to run
Docker Build and Push / integration-test (push) Successful in 5m53s
Docker Build and Push / build (push) Waiting to run
Renovate / renovate (push) Successful in 1m32s
Main Release / hadolint (push) Successful in 37s
Docker Build and Push / test (push) Successful in 4m47s
Reviewed-on: #62
2026-10-18 14:42:34 -04:00
opencodecabilloteu ef480dfd97 ci(tag): dispatch Renovate on tf-depl-kubernetes once the release image is pushed
PR Checks / pytest (pull_request) Successful in 7m14s
Docker Build and Push / integration-test (pull_request) Successful in 3m54s
PR Checks / hadolint (pull_request) Successful in 28s
PR Checks / build-test (pull_request) Successful in 5m15s
Docker Build and Push / test (pull_request) Successful in 3m40s
Docker Build and Push / build (pull_request) Successful in 3m4s
A release only reached perso_private/tf-depl-kubernetes on the next hourly
Renovate scan, so the image bump PR could lag up to an hour behind the tag.

After the image push completes, dispatch that repo's Renovate workflow through
the Gitea API (workflow_dispatch is already enabled there). It authenticates
with the RENOVATE_TOKEN org secret of this organisation — the same token the
Renovate instance itself uses, no extra credential. The job fails loudly so a
rotated token shows up instead of the trigger silently going missing.
2026-10-18 13:22:32 -04:00
jcabillot 1115db82ed Merge pull request 'ci: harden the qdrant integration job (5s client timeout + cold storage)' (#60) from ci/harden-qdrant-integration into main
Main Release / test (push) Successful in 2m54s
Main Release / integration-test (push) Successful in 3m17s
Main Release / build (push) Successful in 21s
Tag Release / build-push (push) Successful in 3m4s
Docker Build and Push / integration-test (push) Successful in 3m12s
Renovate / renovate (push) Successful in 59s
Main Release / tag (push) Successful in 17s
Docker Build and Push / build (push) Successful in 4m25s
Tag Release / integration-test (push) Successful in 2m35s
Main Release / hadolint (push) Successful in 17s
Docker Build and Push / test (push) Successful in 3m0s
Tag Release / hadolint (push) Successful in 24s
Tag Release / test (push) Successful in 2m42s
Reviewed-on: #60
2026-10-17 17:42:56 -04:00
jcabillot dd41603a0b Merge pull request 'fix: serve MCP over Streamable HTTP instead of legacy SSE' (#58) from fix/streamable-http-transport into main
Main Release / build (push) Canceled after 0s
Main Release / integration-test (push) Canceled after 1m8s
Main Release / test (push) Canceled after 1m10s
Main Release / tag (push) Canceled after 0s
Renovate / renovate (push) Successful in 1m32s
Docker Build and Push / build (push) Skipped
Main Release / hadolint (push) Canceled after 1m12s
Docker Build and Push / test (push) Successful in 2m14s
Docker Build and Push / integration-test (push) Failing after 11m3s
Reviewed-on: #58
2026-10-17 17:42:33 -04:00
Sagent 01d15c486e ci: harden the qdrant integration job
Docker Build and Push / integration-test (pull_request) Successful in 5m30s
PR Checks / pytest (pull_request) Successful in 3m7s
PR Checks / hadolint (pull_request) Successful in 23s
Docker Build and Push / test (pull_request) Successful in 5m19s
PR Checks / build-test (pull_request) Successful in 47s
Docker Build and Push / build (pull_request) Successful in 3m14s
The integration test job has been failing intermittently on main and on
open pull requests with `httpx.ReadTimeout: timed out` on the first write
to Qdrant, while `GET /collections` and `/healthz` kept answering.

Cause: the qdrant-client default request timeout is 5 seconds, and the job
only waits for `/healthz`, which is served before the storage engine
accepts writes. On a shared runner doing several builds at once, the first
collection creation or indexing exceeds 5s and the whole fixture errors
out.

- pin qdrant/qdrant to v1.19 (the digest `latest` and the cluster already
  run) and let Renovate track the pin in the workflows as well
- wait for a real write round-trip (create + delete a probe collection)
  instead of `/healthz` alone, and dump the container logs if it never
  becomes writable
- make the Qdrant client timeout configurable via QDRANT_TIMEOUT and set
  it to 60s in CI, where there is no latency SLA
2026-10-17 21:24:18 +00:00
jcabillot e883a791fb Merge pull request 'fix(indexer): restore module-level embedding/batch constants (cronjobs cassés)' (#59) from fix/indexer-missing-constants into main
Main Release / test (push) Successful in 2m25s
Main Release / integration-test (push) Successful in 2m32s
Main Release / build (push) Successful in 3m56s
Tag Release / build-push (push) Skipped
Docker Build and Push / build (push) Skipped
Main Release / tag (push) Successful in 47s
Renovate / renovate (push) Failing after 14m44s
Tag Release / integration-test (push) Failing after 3m3s
Docker Build and Push / integration-test (push) Failing after 5m58s
Main Release / hadolint (push) Successful in 50s
Docker Build and Push / test (push) Successful in 5m1s
Tag Release / hadolint (push) Successful in 19s
Tag Release / test (push) Successful in 3m17s
Reviewed-on: #59
2026-10-17 17:11:51 -04:00
Sagent 3b575bbf83 fix(indexer): restore module-level embedding/batch constants
Docker Build and Push / integration-test (pull_request) Successful in 2m38s
PR Checks / pytest (pull_request) Successful in 2m19s
PR Checks / hadolint (pull_request) Successful in 12s
Docker Build and Push / test (pull_request) Successful in 2m17s
PR Checks / build-test (pull_request) Successful in 3m5s
Docker Build and Push / build (pull_request) Successful in 2m39s
The "make indexer importable without env" refactor (e32806a, merged via PR
#52 on 2026-10-09) removed EMBEDDING_MODEL_NAME, BATCH_SIZE and
EMBEDDING_BATCH_SIZE from the module scope while main() kept referencing
them, so every indexer run died before reaching Qdrant:

    File "/app/src/indexer.py", line 264, in main
        print(f"Loading embedding model: {EMBEDDING_MODEL_NAME}...")
    NameError: name 'EMBEDDING_MODEL_NAME' is not defined

CI never caught it because no test executes main(). Both *-indexer cronjobs
have been failing (BackoffLimitExceeded) since 2026-10-16, i.e. the first
run after the image pin moved to v0.0.22-latest.

- restore the three constants with their pre-refactor defaults
- cover them in tests/test_indexer.py and cross-check that the indexer
  embeds with the same model as server.py (vectors must stay comparable)
- add a ruff F821 gate to the unit job so undefined names fail CI

Fixes mcp-maildir-cabilloteu-indexer and mcp-maildir-gmail-indexer.
2026-10-17 21:04:34 +00:00
Sagent bc3deb8676 fix: serve MCP over Streamable HTTP instead of legacy SSE
PR Checks / pytest (pull_request) Successful in 2m47s
Docker Build and Push / integration-test (pull_request) Successful in 3m24s
PR Checks / hadolint (pull_request) Successful in 40s
PR Checks / build-test (pull_request) Successful in 3m19s
Docker Build and Push / test (pull_request) Successful in 3m1s
Docker Build and Push / build (pull_request) Successful in 3m4s
Current MCP clients (OpenCode, Claude, ...) only speak Streamable HTTP: they
POST JSON-RPC to the configured URL, so a legacy SSE endpoint answers
405 Method Not Allowed and not a single tool can be reached.

Run FastMCP's http_app (build_app()) under uvicorn instead of mcp.run(sse):
the MCP endpoint is now /mcp, and the custom GET /health route keeps serving
the Kubernetes probes.

Add transport tests asserting /mcp and /health are exposed and /sse is gone.
2026-10-17 20:45:26 +00:00
jcabillot 4d9bd4c470 Merge pull request 'chore(deps): update dependency python to 3.14' (#54) from renovate/python-3.x into main
Main Release / test (push) Successful in 3m26s
Main Release / integration-test (push) Successful in 3m37s
Main Release / build (push) Successful in 46s
Tag Release / build-push (push) Successful in 3m43s
Docker Build and Push / integration-test (push) Successful in 3m47s
Renovate / renovate (push) Successful in 1m29s
Docker Build and Push / build (push) Successful in 4m11s
Main Release / tag (push) Successful in 42s
Tag Release / integration-test (push) Successful in 3m9s
Docker Build and Push / test (push) Successful in 3m29s
Main Release / hadolint (push) Successful in 38s
Tag Release / hadolint (push) Failing after 14m10s
Tag Release / test (push) Successful in 2m54s
Reviewed-on: #54
2026-10-12 15:56:35 -04:00
renovate cc74f384bf chore(deps): update dependency python to 3.14
renovate/stability-days Updates have met minimum release age requirement
PR Checks / pytest (pull_request) Successful in 2m8s
Docker Build and Push / integration-test (pull_request) Successful in 2m19s
PR Checks / hadolint (pull_request) Successful in 18s
Docker Build and Push / test (pull_request) Successful in 2m7s
PR Checks / build-test (pull_request) Successful in 2m23s
Docker Build and Push / build (pull_request) Successful in 1m49s
2026-10-09 21:10:08 +00:00
jcabillot 52642f9d99 Merge pull request 'chore(deps): update dependency fastmcp to v4' (#48) from renovate/fastmcp-4.x into main
Main Release / test (push) Successful in 1m55s
Main Release / integration-test (push) Successful in 2m5s
Main Release / build (push) Successful in 2m39s
Tag Release / build-push (push) Successful in 2m46s
Renovate / renovate (push) Successful in 53s
Docker Build and Push / integration-test (push) Successful in 2m23s
Docker Build and Push / build (push) Successful in 3m10s
Main Release / tag (push) Successful in 18s
Tag Release / integration-test (push) Successful in 1m45s
Main Release / hadolint (push) Successful in 20s
Docker Build and Push / test (push) Successful in 2m25s
Tag Release / hadolint (push) Successful in 14s
Tag Release / test (push) Successful in 1m40s
Reviewed-on: #48
2026-10-09 16:18:09 -04:00
renovate e7c39135ab chore(deps): update dependency fastmcp to v4
renovate/stability-days Updates have met minimum release age requirement
PR Checks / pytest (pull_request) Successful in 2m23s
Docker Build and Push / integration-test (pull_request) Successful in 2m55s
Docker Build and Push / test (pull_request) Successful in 2m21s
PR Checks / hadolint (pull_request) Successful in 33s
PR Checks / build-test (pull_request) Successful in 3m32s
Docker Build and Push / build (pull_request) Successful in 3m6s
2026-10-09 20:04:04 +00:00
jcabillot 916850b692 Merge pull request 'test: run pytest in CI — real unit job + Qdrant-backed integration job' (#52) from tests/ci-pytest into main
Main Release / test (push) Successful in 4m38s
Main Release / integration-test (push) Successful in 3m14s
Main Release / build (push) Successful in 4m8s
Tag Release / build-push (push) Successful in 3m9s
Renovate / renovate (push) Successful in 2m25s
Docker Build and Push / build (push) Successful in 3m51s
Main Release / tag (push) Successful in 52s
Tag Release / integration-test (push) Successful in 2m38s
Main Release / hadolint (push) Successful in 2m18s
Docker Build and Push / test (push) Successful in 4m50s
Docker Build and Push / integration-test (push) Successful in 5m19s
Tag Release / hadolint (push) Successful in 30s
Tag Release / test (push) Successful in 2m25s
Reviewed-on: #52
2026-10-09 15:53:52 -04:00
OpenCode Cabillot 679d72288f test: fix collection name mismatch — env wiring centralized in conftest
PR Checks / pytest (pull_request) Successful in 1m59s
Docker Build and Push / integration-test (pull_request) Successful in 2m14s
PR Checks / hadolint (pull_request) Successful in 18s
Docker Build and Push / test (pull_request) Successful in 2m7s
PR Checks / build-test (pull_request) Successful in 2m28s
Docker Build and Push / build (pull_request) Successful in 1m50s
Root cause of the CI 404s: test modules mutated os.environ at import time
(test_server.py set COLLECTION_NAME='test', test_integration.py
'test_mcp_maildir'). With pytest's alphabetical import order,
server.COLLECTION_NAME froze at whichever value was set when server was
first imported, while the fixture read os.environ at runtime — often a
different name (log proof: fixture created 'test', server queried
'test_mcp_maildir' → 404 on all 6 tests).

conftest.py now sets QDRANT_URL/COLLECTION_NAME/EMBEDDING_MODEL_NAME once,
before any test module import; env mutation removed from test files.
Unit suite still green (43 passed, 6 deselected).
2026-10-09 12:42:21 +00:00
OpenCode Cabillot ef8fe35da7 test: introspect http client base_url safely
Docker Build and Push / integration-test (pull_request) Failing after 2m25s
PR Checks / pytest (pull_request) Successful in 1m50s
PR Checks / hadolint (pull_request) Successful in 23s
PR Checks / build-test (pull_request) Successful in 34s
Docker Build and Push / test (pull_request) Successful in 2m17s
Docker Build and Push / build (pull_request) Skipped
2026-10-09 12:35:55 +00:00
OpenCode Cabillot 6997326b81 test: fix fixture debug attribute (QdrantClient has no rest_uri)
Docker Build and Push / integration-test (pull_request) Failing after 2m4s
PR Checks / pytest (pull_request) Successful in 1m48s
PR Checks / hadolint (pull_request) Successful in 17s
Docker Build and Push / test (pull_request) Successful in 2m2s
PR Checks / build-test (pull_request) Successful in 2m24s
Docker Build and Push / build (pull_request) Skipped
2026-10-09 12:27:53 +00:00
OpenCode Cabillot a597ecf6a2 test: instrument qdrant_setup fixture (log url + collections before/after create)
Docker Build and Push / integration-test (pull_request) Failing after 2m26s
PR Checks / pytest (pull_request) Successful in 2m0s
PR Checks / hadolint (pull_request) Successful in 18s
Docker Build and Push / test (pull_request) Successful in 2m32s
PR Checks / build-test (pull_request) Successful in 2m33s
Docker Build and Push / build (pull_request) Skipped
2026-10-09 12:21:45 +00:00
OpenCode Cabillot aa30111a8e ci: unique qdrant container name per run — parallel workflows on one host clash
Docker Build and Push / integration-test (pull_request) Failing after 4m23s
PR Checks / pytest (pull_request) Successful in 3m52s
PR Checks / hadolint (pull_request) Successful in 41s
Docker Build and Push / test (pull_request) Successful in 4m14s
PR Checks / build-test (pull_request) Successful in 5m26s
Docker Build and Push / build (pull_request) Skipped
Two concurrent jobs (docker-build + PR Checks on the same push) both
used container name 'qdrant'; the second job's 'docker rm -f qdrant'
destroyed the first job's Qdrant mid-run, so the test collection 404'd.
Name is now qdrant-${{ github.run_id }} everywhere (cleanup included).
2026-10-09 01:28:43 +00:00
OpenCode Cabillot 55841310bb ci: use pytest -m integration in docker-build.yaml (marker replaces --ignore file check)
Docker Build and Push / integration-test (pull_request) Failing after 4m52s
PR Checks / pytest (pull_request) Successful in 3m55s
PR Checks / hadolint (pull_request) Successful in 40s
PR Checks / build-test (pull_request) Successful in 1m3s
Docker Build and Push / test (pull_request) Successful in 4m31s
Docker Build and Push / build (pull_request) Skipped
2026-10-09 01:07:34 +00:00
OpenCode Cabillot 9417e577a9 test: mark integration suite with pytest.mark.integration
Docker Build and Push / integration-test (pull_request) Failing after 4m20s
PR Checks / pytest (pull_request) Successful in 3m29s
PR Checks / hadolint (pull_request) Successful in 32s
Docker Build and Push / test (pull_request) Successful in 3m40s
PR Checks / build-test (pull_request) Successful in 4m38s
Docker Build and Push / build (pull_request) Skipped
Required for -m 'not integration' to actually deselect them (pytest.ini
addopts) and for 'pytest -m integration' in the nightly CI job.
2026-10-09 00:20:35 +00:00
OpenCode Cabillot de27820cc7 ci: run pytest in all workflows — real unit job + Qdrant-backed integration job
- pr.yaml: new pytest job (unit tests only, integration deselected by marker)
- main.yaml / cron.yaml / tag.yaml: replace the build-only 'test' job with a
  real pytest unit job + an integration-test job that starts a Qdrant
  container sharing the job container's network namespace
  (--network container:$(hostname) — bypasses the Gitea runner bug with
  service containers), waits for /healthz, runs 'pytest -m integration',
  then tears down.
- build/build-push jobs now require [test, integration-test]; previously
  'test' was only a docker build and pytest never ran in CI.
- integration-test first cleans up any stale qdrant container (rerun-safe).
- setup-python SHA aligned with docker-build.yaml (v7, python 3.13/3.14).
2026-10-09 00:18:25 +00:00
jcabillot 388b345a2f Merge pull request 'ci: add Renovate workflow' (#44) from add-renovate-workflow into main
Main Release / test (push) Successful in 54s
Main Release / build (push) Successful in 54s
Tag Release / build-push (push) Successful in 4m50s
Renovate / renovate (push) Successful in 1m55s
Docker Build and Push / integration-test (push) Successful in 5m24s
Main Release / tag (push) Successful in 54s
Docker Build and Push / build (push) Successful in 6m17s
Main Release / hadolint (push) Successful in 40s
Docker Build and Push / test (push) Successful in 4m31s
Tag Release / hadolint (push) Successful in 42s
Tag Release / test (push) Successful in 56s
Reviewed-on: #44
2026-10-08 20:17:24 -04:00
OpenCode Cabillot b9e22766c0 test: cover build_filter (participant should-clause, date ranges, combination) 2026-10-09 00:16:58 +00:00
OpenCode Cabillot e32806aab9 test: add indexer unit tests + integration marker; make indexer importable without env
- pytest.ini: 'integration' marker, deselected by default; the integration
  suite requires a live Qdrant and will only run in the nightly CI job.
- src/indexer.py: move env validation from import time into _validate_config()
  called by main() — the module can now be imported without MAILDIR_PATH /
  QDRANT_URL / COLLECTION_NAME set, enabling unit tests.
- tests/test_indexer.py: 27 new unit tests, no network and no model:
  decode_mime_words (encoded-words utf-8/iso-8859-1, mixed), extract_text_from_html
  (tags, scripts), normalize_email_address, parse_email_message with real
  EmailMessage objects (plain bodies, attachments detected and excluded from
  body, HTML alternative, unknown charset fallback via raw bytes message,
  encoded attachment filename), get_recent_keys against a real tmp_path
  Maildir (fresh mtime selected, old mtime excluded, colon-suffix keys).
  mailbox.Maildir(create=True) does not create cur/new/tmp on Python 3.11 —
  the test helper pre-creates them.
2026-10-09 00:16:10 +00:00
opencodecabilloteu 6b17ca4ad5 Merge pull request 'chore(deps): update docker/setup-buildx-action digest to f87e599' (#51) from renovate/docker-setup-buildx-action-digest into main
Main Release / test (push) Successful in 2m2s
Main Release / build (push) Successful in 18s
Tag Release / build-push (push) Successful in 1m52s
Docker Build and Push / build (push) Successful in 2m11s
Main Release / tag (push) Successful in 11s
Main Release / hadolint (push) Successful in 11s
Docker Build and Push / test (push) Successful in 1m58s
Docker Build and Push / integration-test (push) Successful in 2m3s
Tag Release / hadolint (push) Successful in 6s
Tag Release / test (push) Successful in 15s
2026-09-18 04:32:13 -04:00
renovate d1c531d8f2 chore(deps): update docker/setup-buildx-action digest to f87e599
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 1m55s
PR Checks / hadolint (pull_request) Successful in 11s
PR Checks / build-test (pull_request) Successful in 26s
Docker Build and Push / test (pull_request) Successful in 1m43s
Docker Build and Push / build (pull_request) Successful in 1m35s
2026-09-18 08:10:55 +00:00
opencodecabilloteu 24b4e090a2 Merge pull request 'chore(deps): update docker/build-push-action digest to c3c9e26' (#49) from renovate/docker-build-push-action-digest into main
Main Release / test (push) Successful in 26s
Main Release / build (push) Successful in 22s
Tag Release / build-push (push) Successful in 2m47s
Docker Build and Push / integration-test (push) Successful in 2m26s
Docker Build and Push / build (push) Successful in 2m35s
Main Release / tag (push) Successful in 11s
Docker Build and Push / test (push) Successful in 2m7s
Main Release / hadolint (push) Successful in 10s
Tag Release / hadolint (push) Successful in 10s
Tag Release / test (push) Successful in 20s
2026-09-17 06:30:15 -04:00
renovate 1f66018225 chore(deps): update docker/build-push-action digest to c3c9e26
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 2m8s
PR Checks / hadolint (pull_request) Successful in 12s
PR Checks / build-test (pull_request) Successful in 26s
Docker Build and Push / test (pull_request) Successful in 1m59s
Docker Build and Push / build (pull_request) Successful in 1m47s
2026-09-17 10:09:45 +00:00
opencodecabilloteu 8ecbfbb613 Merge pull request 'chore(deps): update docker/build-push-action action to v7.4.0' (#50) from renovate/docker-build-push-action-7.x into main
Main Release / test (push) Successful in 2m13s
Main Release / build (push) Successful in 19s
Tag Release / build-push (push) Successful in 2m57s
Docker Build and Push / build (push) Successful in 2m26s
Main Release / tag (push) Successful in 12s
Docker Build and Push / test (push) Successful in 1m55s
Main Release / hadolint (push) Successful in 8s
Docker Build and Push / integration-test (push) Successful in 2m4s
Tag Release / hadolint (push) Successful in 9s
Tag Release / test (push) Successful in 20s
2026-09-17 05:32:23 -04:00
renovate 8a4fdb6d49 chore(deps): update docker/build-push-action action to v7.4.0
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 2m21s
PR Checks / hadolint (pull_request) Successful in 10s
Docker Build and Push / test (pull_request) Successful in 2m6s
PR Checks / build-test (pull_request) Successful in 24s
Docker Build and Push / build (pull_request) Successful in 1m58s
2026-09-17 09:12:14 +00:00
jcabillot 32cd00b8c6 Merge pull request 'chore: remove legacy GitLab CI and Jenkins files' (#47) from ci/remove-legacy-ci into main
Main Release / test (push) Successful in 3m27s
Main Release / build (push) Successful in 36s
Tag Release / build-push (push) Successful in 6m46s
Docker Build and Push / integration-test (push) Successful in 3m56s
Docker Build and Push / build (push) Successful in 9m35s
Main Release / tag (push) Successful in 20s
Main Release / hadolint (push) Successful in 21s
Docker Build and Push / test (push) Successful in 4m54s
Tag Release / hadolint (push) Successful in 12s
Tag Release / test (push) Successful in 29s
Reviewed-on: #47
2026-09-02 09:49:33 -04:00
OpenCode Cabillot c77d8b4fe6 chore: remove legacy GitLab CI and Jenkins files
Docker Build and Push / integration-test (pull_request) Successful in 5m16s
PR Checks / hadolint (pull_request) Successful in 15s
Docker Build and Push / test (pull_request) Successful in 5m0s
PR Checks / build-test (pull_request) Successful in 3m59s
Docker Build and Push / build (pull_request) Successful in 10m15s
2026-09-02 13:46:30 +00:00
opencodecabilloteu 6347ee811d Merge pull request 'chore(deps): update hadolint/hadolint-action action to v3.5.0' (#46) from renovate/hadolint-hadolint-action-3.x into main
Main Release / test (push) Successful in 2m52s
Main Release / build (push) Successful in 30s
Tag Release / build-push (push) Successful in 3m38s
Docker Build and Push / integration-test (push) Successful in 3m20s
Main Release / tag (push) Successful in 16s
Docker Build and Push / build (push) Successful in 3m31s
Docker Build and Push / test (push) Successful in 2m17s
Main Release / hadolint (push) Successful in 13s
Tag Release / hadolint (push) Successful in 14s
Tag Release / test (push) Successful in 28s
2026-08-26 12:01:57 -04:00
renovate 8480e4e322 chore(deps): update hadolint/hadolint-action action to v3.5.0
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 3m54s
PR Checks / hadolint (pull_request) Successful in 16s
Docker Build and Push / test (pull_request) Successful in 2m40s
PR Checks / build-test (pull_request) Successful in 3m26s
Docker Build and Push / build (pull_request) Successful in 3m27s
2026-08-26 15:15:16 +00:00
opencodecabilloteu 99a52ea9ae Merge pull request 'chore(deps): update docker/setup-buildx-action digest to 37fe631' (#45) from renovate/docker-setup-buildx-action-digest into main
Main Release / test (push) Successful in 44s
Main Release / build (push) Successful in 31s
Tag Release / build-push (push) Successful in 3m47s
Main Release / tag (push) Successful in 22s
Docker Build and Push / build (push) Successful in 4m7s
Docker Build and Push / integration-test (push) Successful in 1m13s
Docker Build and Push / test (push) Successful in 56s
Main Release / hadolint (push) Successful in 15s
Tag Release / hadolint (push) Successful in 19s
Tag Release / test (push) Successful in 38s
2026-08-21 06:39:42 -04:00
renovate b4de80928a chore(deps): update docker/setup-buildx-action digest to 37fe631
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 1m10s
Docker Build and Push / test (pull_request) Successful in 51s
PR Checks / hadolint (pull_request) Successful in 20s
PR Checks / build-test (pull_request) Successful in 37s
Docker Build and Push / build (pull_request) Successful in 2m44s
2026-08-21 10:19:07 +00:00
OpenCode Cabillot 478affe4fa ci: add Renovate workflow
Docker Build and Push / integration-test (pull_request) Successful in 1m56s
PR Checks / hadolint (pull_request) Successful in 12s
PR Checks / build-test (pull_request) Successful in 2m4s
Docker Build and Push / test (pull_request) Successful in 2m0s
Docker Build and Push / build (pull_request) In progress
2026-08-02 23:46:53 +00:00
jcabillot eb983ecaef Merge pull request 'chore(deps): update hadolint/hadolint-action action to v3.4.0' (#43) from renovate/hadolint-hadolint-action-3.x into main
Main Release / test (push) Successful in 3m11s
Main Release / build (push) Successful in 27s
Tag Release / build-push (push) Successful in 6m38s
Main Release / tag (push) Successful in 20s
Docker Build and Push / build (push) Skipped
Main Release / hadolint (push) Successful in 12s
Docker Build and Push / test (push) Failing after 3m2s
Tag Release / hadolint (push) Successful in 16s
Docker Build and Push / integration-test (push) Failing after 3h0m4s
Tag Release / test (push) Successful in 28s
Reviewed-on: #43
2026-08-02 16:20:47 -04:00
renovate dba5112951 chore(deps): update hadolint/hadolint-action action to v3.4.0
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Failing after 3h0m4s
PR Checks / hadolint (pull_request) Successful in 15s
PR Checks / build-test (pull_request) Successful in 2m39s
Docker Build and Push / test (pull_request) Failing after 3h0m5s
Docker Build and Push / build (pull_request) Skipped
2026-08-01 14:11:03 +00:00
Julien Cabillot 08e66e74a1 fix: remove mcp from direct dep
Main Release / test (push) Successful in 3m18s
Main Release / build (push) Successful in 2m8s
Tag Release / build-push (push) Successful in 2m41s
Docker Build and Push / integration-test (push) Successful in 4m22s
Main Release / tag (push) Successful in 34s
Docker Build and Push / build (push) Successful in 5m29s
Main Release / hadolint (push) Successful in 15s
Docker Build and Push / test (push) Successful in 2m28s
Tag Release / hadolint (push) Successful in 10s
Tag Release / test (push) Successful in 2m25s
2026-07-30 16:41:11 -04:00
jcabillot f234d70a59 Merge pull request 'chore(deps): update dependency mcp to v1.29.0' (#41) from renovate/mcp-1.x into main
Main Release / test (push) Successful in 2m47s
Main Release / build (push) Successful in 2m46s
Tag Release / build-push (push) Successful in 5m57s
Main Release / tag (push) Successful in 34s
Docker Build and Push / build (push) Skipped
Main Release / hadolint (push) Successful in 10s
Tag Release / hadolint (push) Successful in 20s
Docker Build and Push / integration-test (push) Failing after 3h0m4s
Docker Build and Push / test (push) Failing after 3h0m6s
Tag Release / test (push) Successful in 2m28s
Reviewed-on: #41
2026-07-30 12:39:24 -04:00
renovate f62efe1c14 chore(deps): update dependency mcp to v1.29.0
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 4m35s
PR Checks / hadolint (pull_request) Successful in 28s
PR Checks / build-test (pull_request) Successful in 3m9s
Docker Build and Push / test (pull_request) Successful in 39m7s
Docker Build and Push / build (pull_request) Successful in 6m6s
2026-07-30 14:08:28 +00:00
jcabillot 91b8e041dc Merge pull request 'chore(deps): update dependency fastmcp to v3.4.5' (#40) from renovate/fastmcp-3.x into main
Main Release / test (push) Successful in 2m16s
Main Release / build (push) Successful in 3m31s
Tag Release / build-push (push) Successful in 9m13s
Docker Build and Push / build (push) Successful in 7m32s
Main Release / tag (push) Successful in 30s
Docker Build and Push / integration-test (push) Successful in 2m1s
Main Release / hadolint (push) Successful in 9s
Docker Build and Push / test (push) Successful in 2m48s
Tag Release / hadolint (push) Successful in 37s
Tag Release / test (push) Successful in 5m53s
Reviewed-on: #40
2026-07-29 16:34:42 -04:00
renovate 8e05ff9494 chore(deps): update dependency fastmcp to v3.4.5
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 50s
Docker Build and Push / test (pull_request) Successful in 1m2s
PR Checks / hadolint (pull_request) Successful in 13s
PR Checks / build-test (pull_request) Successful in 2m55s
Docker Build and Push / build (pull_request) Successful in 8m24s
2026-07-29 20:08:04 +00:00
jcabillot 6c56e82df6 Merge pull request 'chore(deps): update docker/login-action digest to dbcb813' (#39) from renovate/docker-login-action-digest into main
Main Release / test (push) Successful in 2m57s
Main Release / build (push) Successful in 3m27s
Tag Release / build-push (push) Successful in 6m37s
Docker Build and Push / build (push) Successful in 3m10s
Main Release / tag (push) Successful in 38s
Main Release / hadolint (push) Successful in 13s
Docker Build and Push / integration-test (push) Successful in 3m5s
Docker Build and Push / test (push) Successful in 2m45s
Tag Release / hadolint (push) Successful in 10s
Tag Release / test (push) Successful in 2m46s
Reviewed-on: #39
2026-07-29 11:47:38 -04:00
renovate 1147915483 chore(deps): update docker/login-action digest to dbcb813
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 39s
Docker Build and Push / test (pull_request) Successful in 3m31s
PR Checks / hadolint (pull_request) Successful in 16s
PR Checks / build-test (pull_request) Successful in 3m4s
Docker Build and Push / build (pull_request) Successful in 6m7s
2026-07-29 13:09:36 +00:00
jcabillot d648c6f133 Merge pull request 'chore(deps): update docker/login-action digest to af1e73f' (#32) from renovate/docker-login-action-digest into main
Main Release / test (push) Successful in 25s
Main Release / build (push) Successful in 22s
Tag Release / build-push (push) Successful in 5m21s
Docker Build and Push / build (push) Successful in 2m53s
Main Release / tag (push) Successful in 14s
Docker Build and Push / test (push) Successful in 23s
Docker Build and Push / integration-test (push) Successful in 35s
Main Release / hadolint (push) Successful in 11s
Tag Release / hadolint (push) Successful in 10s
Tag Release / test (push) Successful in 17s
Reviewed-on: #32
Reviewed-by: opencodecabilloteu <julien+opencode@cabillot.eu>
2026-07-23 07:56:32 -04:00
jcabillot b498341e32 Merge pull request 'chore(deps): update actions/setup-python action to v7' (#38) from renovate/actions-setup-python-7.x into main
Main Release / test (push) Successful in 23s
Main Release / build (push) Successful in 22s
Tag Release / build-push (push) Successful in 4m52s
Docker Build and Push / build (push) Skipped
Main Release / tag (push) Successful in 13s
Docker Build and Push / test (push) Failing after 2m3s
Docker Build and Push / integration-test (push) Successful in 3m9s
Main Release / hadolint (push) Successful in 9s
Tag Release / hadolint (push) Successful in 10s
Tag Release / test (push) Successful in 2m19s
Reviewed-on: #38
2026-07-23 07:56:26 -04:00
renovate 695ce771b2 chore(deps): update actions/setup-python action to v7
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 31s
Docker Build and Push / test (pull_request) Successful in 1m4s
PR Checks / hadolint (pull_request) Successful in 10s
PR Checks / build-test (pull_request) Successful in 22s
Docker Build and Push / build (pull_request) Successful in 2m22s
2026-07-22 21:29:18 +00:00
renovate 0a5c8b883d chore(deps): update docker/login-action digest to af1e73f
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 35s
Docker Build and Push / test (pull_request) Successful in 34s
PR Checks / hadolint (pull_request) Successful in 11s
PR Checks / build-test (pull_request) Successful in 20s
Docker Build and Push / build (pull_request) Successful in 2m17s
2026-07-22 21:29:12 +00:00
opencodecabilloteu 661397bebb Merge pull request 'chore(deps): update docker/setup-buildx-action digest to bb05f3f' (#34) from renovate/docker-setup-buildx-action-digest into main
Main Release / test (push) Successful in 20s
Main Release / build (push) Successful in 23s
Docker Build and Push / build (push) Successful in 3m40s
Main Release / tag (push) Failing after 3h0m2s
Docker Build and Push / test (push) Successful in 2m34s
Docker Build and Push / integration-test (push) Successful in 40s
Main Release / hadolint (push) Successful in 9s
2026-07-22 17:28:28 -04:00
renovate 60ef3782a2 chore(deps): update docker/setup-buildx-action digest to bb05f3f
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / integration-test (pull_request) Successful in 29s
Docker Build and Push / test (pull_request) Successful in 20s
PR Checks / hadolint (pull_request) Successful in 10s
PR Checks / build-test (pull_request) Successful in 4m50s
Docker Build and Push / build (pull_request) Successful in 2m9s
2026-07-22 12:07:32 +00:00
jcabillot 734af981d8 Merge pull request 'chore(deps): update actions/checkout digest to 3d3c42e' (#37) from renovate/actions-checkout-digest into main
Main Release / test (push) Successful in 2m19s
Main Release / build (push) Successful in 26s
Tag Release / build-push (push) Successful in 4m29s
Docker Build and Push / build (push) Successful in 10m23s
Main Release / tag (push) Successful in 10s
Docker Build and Push / integration-test (push) Successful in 36s
Main Release / hadolint (push) Successful in 9s
Docker Build and Push / test (push) Successful in 2m3s
Tag Release / hadolint (push) Successful in 10s
Tag Release / test (push) Successful in 3m11s
Reviewed-on: #37
2026-07-22 08:05:10 -04:00
renovate 710c4e84a1 chore(deps): update actions/checkout digest to 3d3c42e
renovate/stability-days Updates have met minimum release age requirement
PR Checks / build-test (pull_request) Successful in 17s
Docker Build and Push / build (pull_request) Successful in 1m52s
PR Checks / hadolint (pull_request) Successful in 6s
Docker Build and Push / test (pull_request) Successful in 28s
Docker Build and Push / integration-test (pull_request) Successful in 30s
2026-07-20 16:09:36 +00:00
opencodecabilloteu 5c2d4bb132 Merge pull request 'chore(deps): update docker/build-push-action action to v7.3.0' (#35) from renovate/docker-build-push-action-7.x into main
Docker Build and Push / integration-test (push) Successful in 25s
Main Release / test (push) Successful in 13s
Main Release / tag (push) Successful in 11s
Docker Build and Push / test (push) Successful in 17s
Main Release / build (push) Successful in 11s
Tag Release / test (push) Successful in 11s
Tag Release / build-push (push) Successful in 2m9s
Main Release / hadolint (push) Successful in 8s
Tag Release / hadolint (push) Successful in 7s
Docker Build and Push / build (push) Successful in 2m8s
2026-07-16 18:07:35 -04:00
renovate 3787e2811b chore(deps): update docker/build-push-action action to v7.3.0
renovate/stability-days Updates have met minimum release age requirement
PR Checks / build-test (pull_request) Successful in 14s
Docker Build and Push / build (pull_request) Successful in 1m22s
PR Checks / hadolint (pull_request) Successful in 6s
Docker Build and Push / test (pull_request) Successful in 17s
Docker Build and Push / integration-test (pull_request) Successful in 24s
2026-07-16 21:04:58 +00:00
jcabillot 27949043df Merge pull request 'chore(deps): update actions/checkout action to v7' (#36) from renovate/actions-checkout-7.x into main
Main Release / test (push) Successful in 14s
Docker Build and Push / integration-test (push) Successful in 21s
Main Release / tag (push) Successful in 10s
Docker Build and Push / test (push) Successful in 17s
Main Release / build (push) Successful in 11s
Tag Release / test (push) Successful in 14s
Tag Release / build-push (push) Successful in 4m5s
Main Release / hadolint (push) Successful in 9s
Tag Release / hadolint (push) Successful in 6s
Docker Build and Push / build (push) Successful in 2m3s
Reviewed-on: #36
2026-07-16 16:26:49 -04:00
renovate ce1d99b7bd chore(deps): update actions/checkout action to v7
renovate/stability-days Updates have met minimum release age requirement
PR Checks / build-test (pull_request) Successful in 13s
Docker Build and Push / build (pull_request) Successful in 1m24s
PR Checks / hadolint (pull_request) Successful in 8s
Docker Build and Push / test (pull_request) Successful in 17s
Docker Build and Push / integration-test (pull_request) Successful in 24s
2026-07-16 18:05:01 +00:00
jcabillot 250fdd27ee Merge pull request 'chore(deps): update docker/metadata-action digest to dc80280' (#33) from renovate/docker-metadata-action-digest into main
Main Release / test (push) Successful in 13s
Docker Build and Push / integration-test (push) Successful in 23s
Main Release / tag (push) Successful in 37s
Docker Build and Push / test (push) Successful in 16s
Main Release / build (push) Successful in 15s
Tag Release / test (push) Successful in 19s
Tag Release / build-push (push) Successful in 2m36s
Main Release / hadolint (push) Successful in 6s
Tag Release / hadolint (push) Successful in 8s
Docker Build and Push / build (push) Successful in 2m34s
Reviewed-on: #33
2026-07-16 13:12:14 -04:00
renovate cef47a2bfb chore(deps): update docker/metadata-action digest to dc80280
renovate/stability-days Updates have met minimum release age requirement
PR Checks / build-test (pull_request) Successful in 16s
Docker Build and Push / build (pull_request) Successful in 1m34s
PR Checks / hadolint (pull_request) Successful in 7s
Docker Build and Push / test (pull_request) Successful in 16s
Docker Build and Push / integration-test (pull_request) Successful in 31s
2026-07-16 17:05:16 +00:00
jcabillot fce1bc25db Merge pull request 'CI: standardize workflows' (#16) from fix/workflow-standard into main
Docker Build and Push / integration-test (push) Successful in 26s
Main Release / test (push) Successful in 2m31s
Main Release / tag (push) Successful in 31s
Docker Build and Push / test (push) Successful in 15s
Main Release / build (push) Successful in 15s
Tag Release / test (push) Successful in 2m17s
Tag Release / build-push (push) Successful in 4m24s
Main Release / hadolint (push) Successful in 8s
Docker Build and Push / build (push) Successful in 2m33s
Tag Release / hadolint (push) Successful in 8s
Reviewed-on: #16
2026-07-16 12:29:17 -04:00
cloudix_mcp_server 731ee8fe9d Add pr.yaml workflow — hadolint + build-test for PR checks
PR Checks / build-test (pull_request) Successful in 2m32s
Docker Build and Push / build (pull_request) Successful in 4m9s
PR Checks / hadolint (pull_request) Successful in 18s
Docker Build and Push / integration-test (pull_request) Successful in 1m9s
Docker Build and Push / test (pull_request) Successful in 2m15s
2026-07-16 12:29:01 -04:00
cloudix_mcp_server 5400697503 Add tag.yaml workflow — hadolint→test→build-push on tag push 2026-07-16 12:29:01 -04:00
cloudix_mcp_server 370937df9b Add cron.yaml workflow — nightly hadolint→test→build-push 2026-07-16 12:29:01 -04:00
cloudix_mcp_server ab974e40f6 Add main.yaml workflow — hadolint→test→build→tag for push to main 2026-07-16 12:29:01 -04:00
jcabillot 8aaac862f3 Merge pull request 'chore(deps): update dependency uvicorn to v0.51.0' (#30) from renovate/uvicorn-0.x into main
Docker Build and Push / integration-test (push) Successful in 24s
Docker Build and Push / test (push) Successful in 21s
Docker Build and Push / build (push) Successful in 2m49s
Reviewed-on: #30
2026-07-13 16:20:34 -04:00
jcabillot 7dfc9018df Merge pull request 'chore(deps): update dependency fastmcp to v3.4.4' (#31) from renovate/fastmcp-3.x into main
Docker Build and Push / integration-test (push) Successful in 29s
Docker Build and Push / test (push) Successful in 24s
Docker Build and Push / build (push) Successful in 2m43s
Reviewed-on: #31
2026-07-13 16:20:30 -04:00
renovate 29f8a7e88a chore(deps): update dependency fastmcp to v3.4.4
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 1m7s
Docker Build and Push / test (pull_request) Successful in 17s
Docker Build and Push / integration-test (pull_request) Successful in 21s
2026-07-11 01:04:36 +00:00
renovate e9512f54e5 chore(deps): update dependency uvicorn to v0.51.0
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 1m9s
Docker Build and Push / test (pull_request) Successful in 16s
Docker Build and Push / integration-test (pull_request) Successful in 23s
2026-07-10 11:04:19 +00:00
jcabillot 0a2c24f533 Merge pull request 'chore(deps): update dependency fastmcp to v3.4.3' (#28) from renovate/fastmcp-3.x into main
Docker Build and Push / integration-test (push) Successful in 27s
Docker Build and Push / test (push) Successful in 18s
Docker Build and Push / build (push) Successful in 2m13s
Reviewed-on: #28
2026-07-09 12:22:45 -04:00
renovate ef33d685ab chore(deps): update dependency fastmcp to v3.4.3
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 1m26s
Docker Build and Push / test (pull_request) Successful in 19s
Docker Build and Push / integration-test (pull_request) Successful in 25s
2026-07-09 13:04:17 +00:00
jcabillot 232291e2cf Merge pull request 'chore(deps): update dependency uvicorn to v0.50.2' (#29) from renovate/uvicorn-0.x into main
Docker Build and Push / integration-test (push) Successful in 27s
Docker Build and Push / test (push) Successful in 19s
Docker Build and Push / build (push) Successful in 2m41s
Reviewed-on: #29
2026-07-09 08:08:25 -04:00
renovate e79f51b4be chore(deps): update dependency uvicorn to v0.50.2
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 1m19s
Docker Build and Push / test (pull_request) Successful in 18s
Docker Build and Push / integration-test (pull_request) Successful in 23s
2026-07-08 11:04:30 +00:00
jcabillot 5376290e69 Merge pull request 'chore(deps): update dependency uvicorn to v0.50.0' (#27) from renovate/uvicorn-0.x into main
Docker Build and Push / integration-test (push) Successful in 30s
Docker Build and Push / test (push) Failing after 13m41s
Docker Build and Push / build (push) Has been skipped
Reviewed-on: #27
2026-07-06 12:08:54 -04:00
renovate d23e40b7ec chore(deps): update dependency uvicorn to v0.50.0
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 1m44s
Docker Build and Push / test (pull_request) Successful in 17s
Docker Build and Push / integration-test (pull_request) Successful in 22s
2026-07-06 05:21:19 +00:00
jcabillot 76d1d3c74c Merge pull request 'chore(deps): update docker/login-action digest to af1e73f' (#26) from renovate/docker-login-action-digest into main
Docker Build and Push / integration-test (push) Successful in 27s
Docker Build and Push / test (push) Successful in 1m59s
Docker Build and Push / build (push) Successful in 3m14s
Reviewed-on: #26
2026-07-03 16:20:08 -04:00
renovate 76586578d0 chore(deps): update docker/login-action digest to af1e73f
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 2m8s
Docker Build and Push / test (pull_request) Successful in 1m51s
Docker Build and Push / integration-test (pull_request) Successful in 2m16s
2026-07-03 14:15:28 +00:00
jcabillot 6e245949ed Merge pull request 'chore(deps): update docker/metadata-action digest to dc80280' (#24) from renovate/docker-metadata-action-digest into main
Docker Build and Push / integration-test (push) Successful in 22s
Docker Build and Push / test (push) Successful in 18s
Docker Build and Push / build (push) Successful in 5m2s
Reviewed-on: #24
2026-07-02 09:37:35 -04:00
renovate 1819478cd6 chore(deps): update docker/metadata-action digest to dc80280
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 2m7s
Docker Build and Push / test (pull_request) Successful in 22s
Docker Build and Push / integration-test (pull_request) Successful in 43s
2026-07-02 13:32:50 +00:00
jcabillot 8c79b213f5 Merge pull request 'chore(deps): update docker/login-action digest to c99871d' (#23) from renovate/docker-login-action-digest into main
Docker Build and Push / integration-test (push) Successful in 30s
Docker Build and Push / test (push) Successful in 21s
Docker Build and Push / build (push) Successful in 5m13s
Reviewed-on: #23
2026-07-02 09:27:43 -04:00
renovate 94bae3cfb5 chore(deps): update docker/login-action digest to c99871d
renovate/stability-days Updates have met minimum release age requirement
Docker Build and Push / build (pull_request) Successful in 3m25s
Docker Build and Push / test (pull_request) Successful in 2m29s
Docker Build and Push / integration-test (pull_request) Successful in 2m47s
2026-07-02 12:42:45 +00:00
opencodecabilloteu 197e510a78 chore(deps): update docker/setup-buildx-action digest to bb05f3f
Docker Build and Push / integration-test (push) Successful in 1m0s
Docker Build and Push / test (push) Successful in 2m12s
Docker Build and Push / build (push) Successful in 3m9s
chore(deps): update docker/setup-buildx-action digest to bb05f3f
2026-07-02 08:36:40 -04:00
renovate 6ca8d835b1 chore(deps): update docker/setup-buildx-action digest to bb05f3f
Docker Build and Push / build (pull_request) Successful in 1m19s
Docker Build and Push / test (pull_request) Successful in 18s
Docker Build and Push / integration-test (pull_request) Successful in 23s
2026-07-02 09:54:12 +00:00
jcabillot e44cff1b91 Merge pull request 'chore(deps): update docker/build-push-action digest to 53b7df9' (#22) from renovate/docker-build-push-action-digest into main
Docker Build and Push / integration-test (push) Successful in 24s
Docker Build and Push / test (push) Successful in 17s
Docker Build and Push / build (push) Successful in 2m53s
Reviewed-on: #22
2026-07-01 16:20:56 -04:00
renovate f2c9be8d04 chore(deps): update docker/build-push-action digest to 53b7df9
Docker Build and Push / build (pull_request) Successful in 2m39s
Docker Build and Push / test (pull_request) Successful in 19s
Docker Build and Push / integration-test (pull_request) Successful in 2m6s
2026-07-01 15:12:30 +00:00
jcabillot c41cccc49b Merge pull request 'chore(deps): update actions/checkout action to v7' (#18) from renovate/actions-checkout-7.x into main
Docker Build and Push / integration-test (push) Successful in 28s
Docker Build and Push / test (push) Successful in 18s
Docker Build and Push / build (push) Successful in 2m8s
Reviewed-on: #18
2026-06-26 11:21:27 -04:00
jcabillot 8274605557 Merge pull request 'chore(deps): update dependency mcp to v1.28.1' (#17) from renovate/mcp-1.x into main
Docker Build and Push / integration-test (push) Successful in 34s
Docker Build and Push / test (push) Successful in 19s
Docker Build and Push / build (push) Successful in 2m6s
Reviewed-on: #17
2026-06-26 11:20:19 -04:00
renovate f2ca2c0b3d chore(deps): update dependency mcp to v1.28.1
Docker Build and Push / build (pull_request) Successful in 1m15s
Docker Build and Push / integration-test (pull_request) Successful in 32s
Docker Build and Push / test (pull_request) Successful in 44s
2026-06-26 13:02:03 +00:00
renovate 5ce8c44bc7 chore(deps): update actions/checkout action to v7
Docker Build and Push / build (pull_request) Successful in 1m7s
Docker Build and Push / test (pull_request) Successful in 16s
Docker Build and Push / integration-test (pull_request) Successful in 22s
2026-06-24 13:10:51 +00:00
jcabillot b997383f81 Merge pull request 'chore(deps): update actions/setup-python digest to ece7cb0' (#21) from renovate/actions-setup-python-digest into main
Docker Build and Push / integration-test (push) Successful in 27s
Docker Build and Push / test (push) Successful in 1m52s
Docker Build and Push / build (push) Successful in 2m19s
Reviewed-on: #21
2026-06-24 08:55:32 -04:00
renovate 58de02858d chore(deps): update actions/setup-python digest to ece7cb0
Docker Build and Push / build (pull_request) Successful in 1m21s
Docker Build and Push / integration-test (pull_request) Successful in 40s
Docker Build and Push / test (pull_request) Successful in 44s
2026-06-24 04:07:27 +00:00
opencodecabilloteu f285cf755c Merge pull request 'test: tea pr creation demo' (#20) from test/tea-pr-creation into main
Docker Build and Push / integration-test (push) Failing after 14m17s
Docker Build and Push / test (push) Failing after 14m17s
Docker Build and Push / build (push) Has been skipped
2026-06-22 12:13:15 -04:00
opencodecabilloteu edbf2e46ff test: tea pr creation demo
Docker Build and Push / build (pull_request) Successful in 1m24s
Docker Build and Push / test (pull_request) Successful in 20s
Docker Build and Push / integration-test (pull_request) Successful in 25s
2026-06-22 16:12:05 +00:00
jcabillot 5158b0b093 Merge pull request 'fix: cleanup Qdrant container before and after integration tests' (#19) from fix/qdrant-cleanup into main
Docker Build and Push / integration-test (push) Successful in 29s
Docker Build and Push / test (push) Successful in 15s
Docker Build and Push / build (push) Successful in 2m19s
Reviewed-on: #19
2026-06-22 12:04:46 -04:00
opencodecabilloteu c8e4d68238 fix: cleanup Qdrant container before and after integration tests
Docker Build and Push / build (pull_request) Successful in 1m29s
Docker Build and Push / test (pull_request) Successful in 20s
Docker Build and Push / integration-test (pull_request) Successful in 26s
2026-06-22 11:53:58 -04:00
jcabillot 33f2ee388f Merge pull request 'chore(deps): update dependency python to 3.14' (#14) from renovate/python-3.x into main
Docker Build and Push / integration-test (push) Successful in 1m58s
Docker Build and Push / test (push) Successful in 1m32s
Docker Build and Push / build (push) Successful in 4m5s
Reviewed-on: #14
2026-06-12 10:54:18 -04:00
jcabillot 0d88ec0354 Merge pull request 'chore(deps): pin dependencies' (#13) from renovate/pin-dependencies into main
Docker Build and Push / integration-test (push) Failing after 32s
Docker Build and Push / test (push) Successful in 14s
Docker Build and Push / build (push) Has been skipped
Reviewed-on: #13
2026-06-12 09:05:13 -04:00
renovate b20a96f2c4 chore(deps): update dependency python to 3.14
Docker Build and Push / build (pull_request) Successful in 3m48s
Docker Build and Push / test (pull_request) Successful in 1m20s
Docker Build and Push / integration-test (pull_request) Successful in 2m4s
2026-06-12 12:56:43 +00:00
renovate f06a391e7d chore(deps): pin dependencies
Docker Build and Push / build (pull_request) Successful in 1m14s
Docker Build and Push / test (pull_request) Successful in 47s
Docker Build and Push / integration-test (pull_request) Successful in 1m1s
2026-06-12 12:56:29 +00:00
jcabillot 482e97fd2d Merge pull request 'chore: add unit tests for pure functions + CI test job' (#11) from chore/add-tests into main
Docker Build and Push / integration-test (push) Failing after 7s
Docker Build and Push / test (push) Successful in 17s
Docker Build and Push / build (push) Has been skipped
Reviewed-on: #11
2026-06-12 08:50:30 -04:00
cloudix_mcp_server 30ac4ae9ca fix: correct integration test assertions for semantic search
Docker Build and Push / build (pull_request) Successful in 1m12s
Docker Build and Push / test (pull_request) Successful in 14s
Docker Build and Push / integration-test (pull_request) Successful in 1m44s
- test_search_by_content: format_search_result() does not include
  body_text, so check for the expected message_id instead.
- test_search_no_results: vector cosine similarity always returns
  nearest neighbors; use a date filter far in the future to
  guarantee zero results instead.
2026-06-12 08:25:52 -04:00
cloudix_mcp_server d784cd9fba fix: share job container network with Qdrant
Docker Build and Push / build (pull_request) Has been skipped
Docker Build and Push / test (pull_request) Successful in 13s
Docker Build and Push / integration-test (pull_request) Failing after 1m49s
docker run -d publishes ports to the Docker bridge network, but the
job container runs on a different Gitea Actions network. This causes
Connection refused when tests try http://localhost:6333.

Use --network container:$(hostname) so Qdrant shares the job container's
network stack, making localhost reachable.
2026-06-12 08:14:35 -04:00
cloudix_mcp_server 14650619ad fix: replace Gitea services container with manual docker run for Qdrant
Docker Build and Push / build (pull_request) Has been skipped
Docker Build and Push / test (pull_request) Successful in 1m33s
Docker Build and Push / integration-test (pull_request) Failing after 2m23s
The Gitea Actions runner v1.0.8 fails to start the Qdrant service container
with 'exec: ./entrypoint.sh: no such file or directory'. This is a runner bug
with how it handles service container entrypoints/CMD.

Bypass the service container mechanism by starting Qdrant manually with
docker run -d and a health check wait loop.
2026-06-12 08:10:51 -04:00
cloudix_mcp_server 655af15121 fix: replace Gitea services container with manual docker run for Qdrant
The Gitea Actions runner v1.0.8 fails to start the Qdrant service container
with 'exec: ./entrypoint.sh: no such file or directory'. This is a runner bug
with how it handles service container entrypoints/CMD.

Bypass the service container mechanism by starting Qdrant manually with
docker run -d and a health check wait loop.
2026-06-12 08:08:18 -04:00
jcabillot e19a9515ca Merge pull request 'chore(deps): update dependency starlette to v1.3.1' (#12) from renovate/starlette-1.x into main
Docker Build and Push / build (push) Successful in 2m20s
Reviewed-on: #12
2026-06-12 07:32:39 -04:00
renovate 6c1e092bb4 chore(deps): update dependency starlette to v1.3.1
Docker Build and Push / build (pull_request) Successful in 1m10s
2026-06-12 09:35:42 +00:00
Sagent 07e7de2811 chore: add integration tests with real Qdrant instance
Docker Build and Push / build (pull_request) Has been skipped
Docker Build and Push / test (pull_request) Successful in 13s
Docker Build and Push / integration-test (pull_request) Failing after 8s
- Integration tests for search_emails and read_email against live Qdrant
- Indexes 3 test emails, tests search by content/participant/date
- CI: new 'integration-test' job with qdrant service, runs before build
- Unit test job ignores integration test file
2026-06-11 12:30:59 +00:00
Sagent 78ce84f4ff fix: set QDRANT_URL/COLLECTION_NAME env vars for test import
Docker Build and Push / build (pull_request) Successful in 1m11s
Docker Build and Push / test (pull_request) Successful in 1m30s
2026-06-11 12:24:46 +00:00
Sagent 1700877918 chore: add unit tests for pure functions + CI test job
Docker Build and Push / build (pull_request) Has been skipped
Docker Build and Push / test (pull_request) Failing after 1m35s
- Unit tests for normalize_email_address, payload_matches_participant,
  format_search_result (9 test cases across 3 test classes)
- New 'test' job in CI workflow (runs before build)
- pytest.ini for pythonpath config
2026-06-11 12:18:20 +00:00
jcabillot 02eacb340e Merge pull request 'chore(deps): update dependency uvicorn to v0.49.0' (#10) from renovate/uvicorn-0.x into main
Docker Build and Push / build (push) Successful in 2m0s
Reviewed-on: #10
2026-06-11 08:01:43 -04:00
renovate 0346a69b4c chore(deps): update dependency uvicorn to v0.49.0
Docker Build and Push / build (pull_request) Successful in 1m10s
2026-06-11 11:58:27 +00:00
jcabillot ddb801a1e8 Merge pull request 'chore(deps): update dependency starlette to v1.3.0' (#9) from renovate/starlette-1.x into main
Docker Build and Push / build (push) Successful in 1m59s
Reviewed-on: #9
2026-06-11 07:46:20 -04:00
jcabillot 6347071a07 Merge pull request 'chore(config): migrate Renovate config' (#6) from renovate/migrate-config into main
Docker Build and Push / build (push) Successful in 1m57s
Reviewed-on: #6
2026-06-11 07:41:42 -04:00
jcabillot 3f51d7d559 Merge pull request 'chore(deps): update dependency mcp to v1.27.2' (#8) from renovate/mcp-1.x into main
Docker Build and Push / build (push) Successful in 2m26s
Reviewed-on: #8
2026-06-11 07:10:02 -04:00
renovate a837e58376 chore(deps): update dependency starlette to v1.3.0
Docker Build and Push / build (pull_request) Successful in 1m8s
2026-06-11 08:15:30 +00:00
renovate 29d69c589e chore(config): migrate config renovate.json
Docker Build and Push / build (pull_request) Successful in 1m18s
2026-06-11 03:14:03 +00:00
renovate f14bb778b8 chore(deps): update dependency mcp to v1.27.2
Docker Build and Push / build (pull_request) Successful in 1m25s
2026-06-11 03:13:38 +00:00
20 changed files with 1220 additions and 78 deletions

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
name: Nightly Rebuild
on:
schedule:
- cron: '0 0 * * *'
jobs:
hadolint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
continue-on-error: true
with:
dockerfile: pkg/Dockerfile
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt pytest
- name: Run unit tests
run: pytest tests/ -v
env:
# indexer/server read env at import; unit tests need no real backend
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Cleanup old Qdrant container
run: |
docker rm -f qdrant-${{ github.run_id }} 2>/dev/null || true
- name: Start Qdrant
run: |
docker run -d --name qdrant-${{ github.run_id }} \
--network "container:$(hostname)" \
docker.io/qdrant/qdrant:latest
- name: Wait for Qdrant
run: |
for i in $(seq 1 30); do
curl -s http://localhost:6333/healthz \
&& echo "QDRANT ready" && break
echo "Waiting for Qdrant... ($i/30)"
sleep 1
done
curl -s http://localhost:6333/healthz > /dev/null
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt pytest
- name: Run integration tests
run: pytest tests/ -v -m integration
env:
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test_mcp_maildir
- name: Cleanup Qdrant
if: always()
run: docker rm -f qdrant-${{ github.run_id }} || true
build-push:
needs: [test, integration-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- id: get-latest-tag
run: |
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
echo "tag=$TAG" >> $GITHUB_OUTPUT
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: jcabillot/mcp-maildir
tags: |
type=raw,value=${{ steps.get-latest-tag.outputs.tag }}-latest,enable=${{ steps.get-latest-tag.outputs.tag != '' }}
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: pkg/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
pull: true
+82 -10
View File
@@ -9,35 +9,107 @@ on:
- cron: '0 0 * * *'
jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: |
pip install -r requirements.txt pytest
- name: Run unit tests
run: pytest tests/ -v --ignore=tests/test_integration.py
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Start Qdrant
run: |
docker rm -f qdrant-${{ github.run_id }} 2>/dev/null || true
docker run -d --name qdrant-${{ github.run_id }} \
--network "container:$(hostname)" \
docker.io/qdrant/qdrant:v1.19
- name: Wait for Qdrant
run: |
# /healthz answers before the storage engine accepts writes, and a cold
# container on a loaded host can take several seconds to create its
# first collection. Gate on a real write round-trip, not on /healthz.
ready=""
for i in $(seq 1 60); do
if curl -sf "http://localhost:6333/healthz" > /dev/null 2>&1 \
&& curl -sf -X PUT "http://localhost:6333/collections/ci_readiness_probe?timeout=60" \
-H 'Content-Type: application/json' \
-d '{"vectors":{"size":4,"distance":"Cosine"}}' > /dev/null 2>&1; then
curl -sf -X DELETE "http://localhost:6333/collections/ci_readiness_probe" > /dev/null 2>&1 || true
ready="yes"
echo "QDRANT ready: write path OK (attempt $i)"
break
fi
echo "Waiting for Qdrant... ($i/60)"
sleep 1
done
if [ -z "$ready" ]; then
echo "Qdrant write path never became ready"
docker logs --tail 100 qdrant-${{ github.run_id }} || true
exit 1
fi
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: |
pip install -r requirements.txt pytest
- name: Run integration tests
run: pytest tests/ -v -m integration
env:
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test_mcp_maildir
# The qdrant client defaults to a 5s request timeout, which a cold
# collection on a shared runner can exceed; this job has no SLA.
QDRANT_TIMEOUT: "60"
- name: Qdrant diagnostics
if: failure()
run: |
docker ps -a || true
docker logs --tail 200 qdrant-${{ github.run_id }} || true
- name: Cleanup Qdrant
if: always()
run: docker rm -f qdrant-${{ github.run_id }} 2>/dev/null || true
build:
runs-on: ubuntu-latest
needs: [test, integration-test]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: jcabillot/mcp-maildir
tags: |
#type=ref,event=branch
#type=ref,event=pr
#type=sha
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
with:
context: .
file: pkg/Dockerfile
+123
View File
@@ -0,0 +1,123 @@
name: Main Release
on:
push:
branches: [main]
jobs:
hadolint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
continue-on-error: true
with:
dockerfile: pkg/Dockerfile
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt pytest ruff==0.17.1
- name: Lint (undefined names)
run: ruff check src/ tests/ --select F821
- name: Run unit tests
run: pytest tests/ -v
env:
# indexer/server read env at import; unit tests need no real backend
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Cleanup old Qdrant container
run: |
docker rm -f qdrant-${{ github.run_id }} 2>/dev/null || true
- name: Start Qdrant
run: |
docker run -d --name qdrant-${{ github.run_id }} \
--network "container:$(hostname)" \
docker.io/qdrant/qdrant:v1.19
- name: Wait for Qdrant
run: |
# /healthz answers before the storage engine accepts writes, and a cold
# container on a loaded host can take several seconds to create its
# first collection. Gate on a real write round-trip, not on /healthz.
ready=""
for i in $(seq 1 60); do
if curl -sf "http://localhost:6333/healthz" > /dev/null 2>&1 \
&& curl -sf -X PUT "http://localhost:6333/collections/ci_readiness_probe?timeout=60" \
-H 'Content-Type: application/json' \
-d '{"vectors":{"size":4,"distance":"Cosine"}}' > /dev/null 2>&1; then
curl -sf -X DELETE "http://localhost:6333/collections/ci_readiness_probe" > /dev/null 2>&1 || true
ready="yes"
echo "QDRANT ready: write path OK (attempt $i)"
break
fi
echo "Waiting for Qdrant... ($i/60)"
sleep 1
done
if [ -z "$ready" ]; then
echo "Qdrant write path never became ready"
docker logs --tail 100 qdrant-${{ github.run_id }} || true
exit 1
fi
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt pytest
- name: Run integration tests
run: pytest tests/ -v -m integration
env:
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test_mcp_maildir
# The qdrant client defaults to a 5s request timeout, which a cold
# collection on a shared runner can exceed; this job has no SLA.
QDRANT_TIMEOUT: "60"
- name: Qdrant diagnostics
if: failure()
run: |
docker ps -a || true
docker logs --tail 200 qdrant-${{ github.run_id }} || true
- name: Cleanup Qdrant
if: always()
run: docker rm -f qdrant-${{ github.run_id }} || true
build:
needs: [test, integration-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- run: docker build -f pkg/Dockerfile -t jcabillot/mcp-maildir:${{ github.sha }} .
tag:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Configure git auth
run: |
git remote set-url origin "https://x-access-token:${{ secrets.SA_TOKEN_ACTION_PUSH_TAGS }}@scm.cabillot.eu/perso/mcp-maildir.git"
- uses: anothrNick/github-tag-action@4ed44965e0db8dab2b466a16da04aec3cc312fd8 # v1.75.0
env:
GITHUB_TOKEN: ${{ secrets.SA_TOKEN_ACTION_PUSH_TAGS }}
DEFAULT_BUMP: patch
RELEASE_BRANCHES: main
WITH_V: true
GIT_API_TAGGING: false
+38
View File
@@ -0,0 +1,38 @@
name: PR Checks
on:
pull_request:
branches: [main]
jobs:
hadolint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
continue-on-error: true
with:
dockerfile: pkg/Dockerfile
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt pytest
- name: Run unit tests
run: pytest tests/ -v
env:
# indexer/server read env at import; unit tests need no real backend
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- run: docker build -f pkg/Dockerfile -t ci-image:${{ github.sha }} .
+12
View File
@@ -0,0 +1,12 @@
name: Renovate
on:
push:
branches:
- main
workflow_dispatch:
jobs:
renovate:
uses: perso/gitea-pipelines/.gitea/workflows/renovate.yaml@main
secrets: inherit
+116
View File
@@ -0,0 +1,116 @@
name: Tag Release
on:
push:
tags: ['*']
jobs:
hadolint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
continue-on-error: true
with:
dockerfile: pkg/Dockerfile
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt pytest
- name: Run unit tests
run: pytest tests/ -v
env:
# indexer/server read env at import; unit tests need no real backend
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Cleanup old Qdrant container
run: |
docker rm -f qdrant-${{ github.run_id }} 2>/dev/null || true
- name: Start Qdrant
run: |
docker run -d --name qdrant-${{ github.run_id }} \
--network "container:$(hostname)" \
docker.io/qdrant/qdrant:latest
- name: Wait for Qdrant
run: |
for i in $(seq 1 30); do
curl -s http://localhost:6333/healthz \
&& echo "QDRANT ready" && break
echo "Waiting for Qdrant... ($i/30)"
sleep 1
done
curl -s http://localhost:6333/healthz > /dev/null
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt pytest
- name: Run integration tests
run: pytest tests/ -v -m integration
env:
QDRANT_URL: http://localhost:6333
COLLECTION_NAME: test_mcp_maildir
- name: Cleanup Qdrant
if: always()
run: docker rm -f qdrant-${{ github.run_id }} || true
build-push:
needs: [test, integration-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: jcabillot/mcp-maildir
tags: |
type=ref,event=tag
type=ref,event=tag,suffix=-latest
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: pkg/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
pull: true
trigger-renovate:
# The deployment repo perso_private/tf-depl-kubernetes pins this image; its
# Renovate instance only scans hourly, so this dispatch opens the image bump
# PR within minutes of the release instead of up to an hour later.
needs: [build-push]
runs-on: ubuntu-latest
steps:
- name: Trigger Renovate on perso_private/tf-depl-kubernetes
# needs: build-push — Renovate reads Docker Hub, so a scan started before
# the push lands would only see the previous release.
# RENOVATE_TOKEN is the org-level secret of this organisation.
run: |
curl -sS -f -X POST \
-u "renovate:${{ secrets.RENOVATE_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{{"ref":"main"}}' \
"https://scm.cabillot.eu/api/v1/repos/perso_private/tf-depl-kubernetes/actions/workflows/renovate.yaml/dispatches"
echo "Renovate dispatched on perso_private/tf-depl-kubernetes"
Vendored
-38
View File
@@ -1,38 +0,0 @@
pipeline {
environment {
registry = 'https://registry.hub.docker.com'
registryCredential = 'dockerhub_jcabillot'
dockerImage = 'jcabillot/mcp-maildir'
}
agent any
triggers {
cron('@midnight')
}
stages {
stage('Clone repository') {
steps{
checkout scm
}
}
stage('Build image') {
steps{
sh 'docker build --force-rm=true --no-cache=true --pull -t ${dockerImage} -f pkg/Dockerfile .'
}
}
stage('Deploy Image') {
steps{
script {
withCredentials([usernamePassword(credentialsId: 'dockerhub_jcabillot', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh 'docker login --username ${DOCKER_USER} --password ${DOCKER_PASS}'
sh 'docker push ${dockerImage}'
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
test
+2 -2
View File
@@ -1,6 +1,6 @@
services:
qdrant:
image: docker.io/qdrant/qdrant:latest
image: docker.io/qdrant/qdrant:v1.19
container_name: mcp_maildir_qdrant
ports:
- "127.0.0.1:6333:6333" # REST API
@@ -15,7 +15,7 @@ services:
dockerfile: pkg/Dockerfile
container_name: mcp_maildir_server
ports:
- "127.0.0.1:8000:8000" # Expose the MCP HTTP (SSE) server
- "127.0.0.1:8000:8000" # Expose the MCP Streamable HTTP server (/mcp)
env_file:
- .env
environment:
+1 -1
View File
@@ -1,5 +1,5 @@
# 3.13, because with 3.14 it requires gcc (wheels not available)
FROM docker.io/library/python:3.14-slim
FROM docker.io/library/python:3.15-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
+8
View File
@@ -0,0 +1,8 @@
[pytest]
testpaths = tests
pythonpath = src
; Integration tests (tests/test_integration.py) require a live Qdrant on
; localhost:6333 — they only run in the nightly CI job (or manually with -m integration).
markers =
integration: requires a live Qdrant instance (deselected by default)
addopts = -m "not integration" -p no:cacheprovider
+7 -5
View File
@@ -1,12 +1,14 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"regexManagers": [
"customManagers": [
{
"description": "Track qdrant/qdrant Docker image from docker-compose.yml (native manager missed it)",
"fileMatch": ["^docker-compose\\.ya?ml$"],
"matchStrings": [
"image:\\s*(?:docker\\.io/)?qdrant/qdrant:(?<currentValue>[^\\s]+)"
"customType": "regex",
"description": "Track the pinned qdrant/qdrant image in docker-compose.yml and the CI workflows (native manager missed it)",
"managerFilePatterns": [
"/^docker-compose\\.ya?ml$/",
"/^\\.gitea/workflows/.*\\.ya?ml$/"
],
"matchStrings": ["(?:docker\\.io/)?qdrant/qdrant:(?<currentValue>[^\\s\"']+)"],
"depNameTemplate": "qdrant/qdrant",
"datasourceTemplate": "docker"
}
+8 -9
View File
@@ -1,9 +1,8 @@
mcp==1.26.0
fastmcp==3.4.2
qdrant-client==1.18.0
fastembed==0.8.0
python-dotenv==1.2.2
uvicorn==0.41.0
starlette==1.2.1
beautifulsoup4==4.15.0
python-dateutil==2.9.0.post0
fastmcp>=4,<5
qdrant-client>=1,<2
fastembed>=0.8.0,<1
python-dotenv>=1,<2
uvicorn>=0.34,<1
starlette>=1.2,<2
beautifulsoup4>=4,<5
python-dateutil>=2,<3
+18 -8
View File
@@ -22,26 +22,34 @@ from bs4 import BeautifulSoup
# Load .env config
load_dotenv()
# Configuration
# Configuration (validated lazily in main() so the module can be imported
# and unit-tested without environment variables)
MAILDIR_PATH = os.environ.get("MAILDIR_PATH", "")
MAILDIR_FOLDERS = os.environ.get("MAILDIR_FOLDERS", "")
QDRANT_URL = os.environ.get("QDRANT_URL", "")
COLLECTION_NAME = os.environ.get("COLLECTION_NAME", "")
if not MAILDIR_PATH:
raise ValueError("MAILDIR_PATH environment variable is required.")
if not QDRANT_URL:
raise ValueError("QDRANT_URL environment variable is required.")
if not COLLECTION_NAME:
raise ValueError("COLLECTION_NAME environment variable is required.")
# Embedding model and batching. EMBEDDING_MODEL_NAME MUST stay in sync with the
# one used by server.py: indexer and server have to embed with the same model,
# otherwise the vectors stored in Qdrant are not comparable with query vectors.
EMBEDDING_MODEL_NAME = os.environ.get("EMBEDDING_MODEL_NAME", "BAAI/bge-small-en-v1.5")
BATCH_SIZE = int(os.environ.get("BATCH_SIZE", "100"))
EMBEDDING_BATCH_SIZE = int(os.environ.get("EMBEDDING_BATCH_SIZE", "64"))
METADATA_COLLECTION = "mcp_indexer_metadata"
INCREMENTAL_DAYS = int(os.environ.get("INCREMENTAL_DAYS", "7"))
FORCE_REINDEX = os.environ.get("FORCE_REINDEX", "").lower() in ("1", "true", "yes")
def _validate_config():
"""Raises if required environment variables are missing. Called from main()."""
if not MAILDIR_PATH:
raise ValueError("MAILDIR_PATH environment variable is required.")
if not QDRANT_URL:
raise ValueError("QDRANT_URL environment variable is required.")
if not COLLECTION_NAME:
raise ValueError("COLLECTION_NAME environment variable is required.")
def decode_mime_words(s: str) -> str:
"""Decodes MIME encoded strings (e.g. subjects, filenames)."""
if not s:
@@ -251,6 +259,8 @@ def main():
Main ingestion function.
Reads Maildir, extracts text, generates local embeddings, and pushes to Qdrant.
"""
_validate_config()
print(f"Indexing emails from {MAILDIR_PATH} into {QDRANT_URL}...")
if not os.path.exists(MAILDIR_PATH):
+26 -5
View File
@@ -9,6 +9,7 @@ from typing import Any, Optional
import qdrant_client as qdrant_pkg
from dateutil import parser as date_parser
from email.utils import parseaddr
import uvicorn
from dotenv import load_dotenv
from starlette.requests import Request
from starlette.responses import JSONResponse
@@ -36,6 +37,10 @@ COLLECTION_NAME = os.environ.get("COLLECTION_NAME", "")
EMBEDDING_MODEL_NAME = os.environ.get("EMBEDDING_MODEL_NAME", "BAAI/bge-small-en-v1.5")
DEFAULT_LIMIT = int(os.environ.get("SEARCH_LIMIT", "50"))
# Per-request timeout (seconds) for Qdrant. The client default is 5s, which is
# tight for a cold collection or a busy host: unset means keep that default.
QDRANT_TIMEOUT = int(os.environ["QDRANT_TIMEOUT"]) if os.environ.get("QDRANT_TIMEOUT") else None
if not QDRANT_URL:
raise ValueError("QDRANT_URL environment variable is required.")
if not COLLECTION_NAME:
@@ -60,8 +65,8 @@ def get_qdrant_client() -> QdrantClient:
"""Returns a singleton Qdrant client."""
global _qdrant_client
if _qdrant_client is None:
logger.info(f"Connecting to Qdrant at {QDRANT_URL}")
_qdrant_client = QdrantClient(url=QDRANT_URL)
logger.info(f"Connecting to Qdrant at {QDRANT_URL} (timeout={QDRANT_TIMEOUT})")
_qdrant_client = QdrantClient(url=QDRANT_URL, timeout=QDRANT_TIMEOUT)
return _qdrant_client
@@ -340,6 +345,23 @@ def read_email(message_id: str):
return {"error": f"read_email failed: {exc}"}
# Streamable HTTP is the transport supported by current MCP clients (OpenCode,
# Claude, ...). The legacy SSE transport is deprecated and is also a POST
# target for clients that only speak Streamable HTTP, which answers 405.
MCP_HOST = "0.0.0.0"
MCP_PORT = 8000
MCP_HTTP_PATH = "/mcp"
def build_app():
"""Builds the ASGI app serving the MCP endpoint over Streamable HTTP.
The MCP endpoint lives at MCP_HTTP_PATH; the custom route declared above
keeps serving GET /health for the Kubernetes probes.
"""
return mcp.http_app(path=MCP_HTTP_PATH)
if __name__ == "__main__":
logger.info("Initializing models before starting server...")
try:
@@ -348,6 +370,5 @@ if __name__ == "__main__":
except Exception as e:
logger.error(f"Failed to load models: {e}")
logger.info("Starting SSE server on 0.0.0.0:8000...")
# Start the MCP server using SSE (Server-Sent Events) over HTTP
mcp.run(transport="sse", host="0.0.0.0", port=8000)
logger.info(f"Starting Streamable HTTP server on {MCP_HOST}:{MCP_PORT}{MCP_HTTP_PATH}...")
uvicorn.run(build_app(), host=MCP_HOST, port=MCP_PORT)
+15
View File
@@ -0,0 +1,15 @@
"""Shared test configuration — sets env vars ONCE before any test module runs.
The env must be consistent for the whole pytest session: server.py freezes
COLLECTION_NAME/QDRANT_URL at import time, while the integration fixture
reads os.environ at fixture time. If any test module mutates os.environ after
that (test_server.py used to set COLLECTION_NAME='test'), the fixture creates
a different collection than the one server functions query → 404.
Keep all env wiring here so import order no longer matters.
"""
import os
os.environ.setdefault("QDRANT_URL", "http://localhost:6333")
os.environ.setdefault("COLLECTION_NAME", "test_mcp_maildir")
os.environ.setdefault("EMBEDDING_MODEL_NAME", "BAAI/bge-small-en-v1.5")
+254
View File
@@ -0,0 +1,254 @@
"""Unit tests for mcp-maildir indexer parsing functions.
Pure-function tests: no Qdrant, no embedding model, no network.
A throwaway Maildir is built in tmp_path for get_recent_keys().
"""
import mailbox
import os
import time
import uuid
from datetime import datetime, timezone, timedelta
from email.message import EmailMessage
from email.utils import format_datetime
import pytest
from indexer import (
decode_mime_words,
extract_text_from_html,
normalize_email_address,
parse_email_message,
get_recent_keys,
METADATA_COLLECTION,
EMBEDDING_MODEL_NAME,
BATCH_SIZE,
EMBEDDING_BATCH_SIZE,
)
# ---------------------------------------------------------------------------
# decode_mime_words
# ---------------------------------------------------------------------------
class TestDecodeMimeWords:
def test_empty_and_none(self):
assert decode_mime_words("") == ""
assert decode_mime_words(None) == ""
def test_plain_ascii(self):
assert decode_mime_words("Hello World") == "Hello World"
def test_encoded_word_utf8(self):
# =?utf-8?q?...?= encoded word
assert decode_mime_words("=?utf-8?q?Caf=C3=A9_au_lait?=") == "Café au lait"
def test_encoded_word_iso8859(self):
# decode_mime_words does not strip whitespace around the words
assert decode_mime_words("=?iso-8859-1?q?caf=E9?=") == "café"
def test_mixed_encoded_and_plain(self):
assert decode_mime_words("Re: =?utf-8?q?planning_2026?=") == "Re: planning 2026"
# ---------------------------------------------------------------------------
# extract_text_from_html
# ---------------------------------------------------------------------------
class TestExtractTextFromHtml:
def test_simple_paragraphs(self):
html = "<html><body><p>Hello</p><p>World</p></body></html>"
assert extract_text_from_html(html) == "Hello World"
def test_strips_tags_and_scripts(self):
html = "<div>Sign<span>ature</span><script>evil()</script>End</div>"
text = extract_text_from_html(html)
assert "evil()" not in text
# get_text uses separator=" ", so tags become whitespace
assert "Sign" in text and "ature" in text and "End" in text
def test_fallback_on_invalid_content(self):
# Garbage that would make the parser blow up should not raise
assert extract_text_from_html(None) is None
# ---------------------------------------------------------------------------
# parse_email_message — real mailbox.Message objects (no mocks)
# ---------------------------------------------------------------------------
def build_email(
subject="Hello",
body="This is the body.",
html=None,
attachments=None,
charset="utf-8",
):
"""Builds a real EmailMessage, optionally multipart with attachments."""
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = "Alice <alice@example.com>"
msg["To"] = "Bob <bob@example.com>"
msg["Message-ID"] = "<build-email@example.com>"
msg["Date"] = format_datetime(datetime(2026, 1, 15, 10, 0, 0, tzinfo=timezone.utc))
if html:
msg.set_content(body)
msg.add_alternative(html, subtype="html")
else:
msg.set_content(body, charset=charset)
for filename, content in (attachments or {}).items():
payload = content if isinstance(content, bytes) else content.encode()
msg.add_attachment(payload, maintype="application",
subtype="octet-stream", filename=filename)
return msg
def build_raw_email(raw_bytes: bytes):
"""Parses raw email bytes (for exotic headers without EmailMessage helpers)."""
import email as email_mod
return email_mod.message_from_bytes(raw_bytes)
class TestParseEmailMessage:
def test_plain_text_body(self):
msg = build_email(body="Line one.\nLine two.")
body, attachments = parse_email_message(msg)
assert "Line one." in body and "Line two." in body
assert attachments == []
def test_attachments_detected_and_excluded_from_body(self):
msg = build_email(
body="See attached.",
attachments={"report.pdf": b"%PDF-fake", "notes.txt": b"notes"},
)
body, attachments = parse_email_message(msg)
assert attachments == ["report.pdf", "notes.txt"]
assert "%PDF" not in body
assert "See attached." in body
def test_html_alternative_extracts_text(self):
msg = build_email(
body="Fallback plain text.",
html="<html><body><b>Bold</b> intro</body></html>",
)
body, _ = parse_email_message(msg)
assert "Bold" in body and "intro" in body
def test_unknown_charset_falls_back_not_crash(self):
# A payload claiming an unknown charset must not raise.
# Built from raw bytes: EmailMessage.set_content(charset=...) would
# reject the unknown encoding at build time, real mail does not.
raw = (
b"Subject: broken charset\n"
b"From: alice@example.com\n"
b"To: bob@example.com\n"
b"Content-Type: text/plain; charset=x-unknown-charset\n"
b"Content-Transfer-Encoding: 8bit\n\n"
b"body with broken charset\n"
)
msg = build_raw_email(raw)
body, attachments = parse_email_message(msg)
# Whatever the fallback, we must get a string body and no exception
assert isinstance(body, str)
assert attachments == []
def test_encoded_attachment_filename_decoded(self):
msg = build_email(
body="body",
attachments={},
)
# Add attachment with non-ascii filename via encoded word
msg.add_attachment(b"data", maintype="application", subtype="pdf",
filename="café-rapport.pdf")
body, attachments = parse_email_message(msg)
assert attachments == ["café-rapport.pdf"]
# ---------------------------------------------------------------------------
# normalize_email_address (indexer copy — same contract as server.py)
# ---------------------------------------------------------------------------
class TestIndexerNormalizeEmailAddress:
def test_display_name(self):
assert normalize_email_address("John Doe <john@example.com>") == "john@example.com"
def test_case_and_whitespace(self):
assert normalize_email_address(" USER@Example.COM ") == "user@example.com"
def test_empty(self):
assert normalize_email_address("") == ""
assert normalize_email_address(None) == ""
# ---------------------------------------------------------------------------
# get_recent_keys — real Maildir on disk (tmp_path), mtime-driven
# ---------------------------------------------------------------------------
def make_maildir(root):
"""Opens a Maildir at root — pre-creates cur/new/tmp because mailbox.Maildir
(Python 3.11) does not create them when adding a message."""
for sub in ("cur", "new", "tmp"):
os.makedirs(os.path.join(str(root), sub), exist_ok=True)
return mailbox.Maildir(str(root), create=False)
def write_maildir_message(maildir: mailbox.Maildir, msg: EmailMessage):
"""Adds a message and returns its key."""
return maildir.add(msg)
class TestGetRecentKeys:
def test_recent_file_selected(self, tmp_path):
md = make_maildir(tmp_path)
msg = build_email()
md.add(msg)
md.flush()
# mtime is now → within the window
keys = get_recent_keys(md, days=7)
assert len(keys) == 1
def test_old_file_excluded(self, tmp_path):
md = make_maildir(tmp_path)
md.add(build_email())
md.flush()
# Age every file beyond the cutoff
old = time.time() - 30 * 86400
for path in tmp_path.rglob("*.??*"):
if path.is_file():
os.utime(path, (old, old))
keys = get_recent_keys(md, days=7)
assert len(keys) == 0
def test_key_is_suffixless_filename(self, tmp_path):
"""Maildir keys strip the ':2,S' info suffix — get_recent_keys must match."""
md = make_maildir(tmp_path)
key = md.add(build_email())
md.flush()
keys = get_recent_keys(md, days=7)
assert keys # non-empty set of keys for fresh files
assert key.split(":")[0] in keys
for k in keys:
assert ":" not in k
# ---------------------------------------------------------------------------
# Regression: module-level constants consumed by main()
# ---------------------------------------------------------------------------
def test_indexer_module_constants_are_defined():
"""Regression (e32806a): the "make indexer importable without env" refactor
dropped these three module-level definitions while main() kept using them,
so every indexer run died with NameError before touching Qdrant."""
assert isinstance(EMBEDDING_MODEL_NAME, str) and EMBEDDING_MODEL_NAME
assert isinstance(BATCH_SIZE, int) and BATCH_SIZE > 0
assert isinstance(EMBEDDING_BATCH_SIZE, int) and EMBEDDING_BATCH_SIZE > 0
def test_embedding_model_is_the_same_as_the_server_one():
"""Indexer and server must embed with the same model, otherwise the vectors
stored by the indexer are not comparable with the query vectors."""
from server import EMBEDDING_MODEL_NAME as server_embedding_model
assert EMBEDDING_MODEL_NAME == server_embedding_model
+217
View File
@@ -0,0 +1,217 @@
"""Integration tests for mcp-maildir with a real Qdrant instance.
Env vars (QDRANT_URL, COLLECTION_NAME, EMBEDDING_MODEL_NAME) are set in
tests/conftest.py — don't set them here, they must match server.py's
import-time snapshot for every module in the session.
"""
import os
import uuid
import pytest
from server import get_qdrant_client, get_embedding_model, search_emails, read_email
from qdrant_client.http import models
# Requires a live Qdrant on localhost:6333 (see pytest.ini: deselected by
# default; run with `pytest -m integration`, nightly CI does exactly that).
pytestmark = pytest.mark.integration
TEST_EMAILS = [
{
"message_id": "<test-001@example.com>",
"date": "2026-01-15T10:00:00",
"sender": "alice@example.com",
"sender_raw": "Alice <alice@example.com>",
"receiver": "bob@example.com",
"receiver_raw": "Bob <bob@example.com>",
"subject": "Hello World",
"body_text": "This is a test email about Python programming and vector databases.",
"attachments": [],
},
{
"message_id": "<test-002@example.com>",
"date": "2026-01-16T14:30:00",
"sender": "carol@other.com",
"sender_raw": "Carol <carol@other.com>",
"receiver": "alice@example.com",
"receiver_raw": "Alice <alice@example.com>",
"subject": "Qdrant setup help",
"body_text": "Can you help me set up Qdrant for semantic email search?",
"attachments": ["screenshot.png"],
},
{
"message_id": "<test-003@example.com>",
"date": "2026-02-01T09:00:00",
"sender": "bob@example.com",
"sender_raw": "Bob <bob@example.com>",
"receiver": "alice@example.com",
"receiver_raw": "Alice <alice@example.com>",
"subject": "Meeting next week",
"body_text": "Let's discuss the project roadmap and data pipeline.",
"attachments": [],
},
]
@pytest.fixture(scope="module")
def qdrant_setup():
"""Create collection and index test emails once per test run."""
client = get_qdrant_client()
model = get_embedding_model()
collection_name = os.environ["COLLECTION_NAME"]
# Debug instrumentalisation — truth about which server we are talking to
# and what it holds (CI 404 diagnosis).
import logging
dbg = logging.getLogger("integration.debug")
try:
dbg.warning("qdrant client url: %s", client.http._client.base_url)
except Exception:
dbg.warning("qdrant client url: <introspection failed>")
server_info = None
try:
server_info = client.get_collections()
dbg.warning("collections BEFORE: %s", [c.name for c in server_info.collections])
except Exception as e:
dbg.warning("get_collections FAILED: %s", e)
# Clean up from previous runs
try:
client.delete_collection(collection_name)
except Exception:
pass
# Create collection
probe = next(iter(model.embed(["dimension_probe"])))
client.create_collection(
collection_name=collection_name,
vectors_config=models.VectorParams(
size=len(probe), distance=models.Distance.COSINE
),
)
# Assert the collection is actually visible right after creation
names_after = [c.name for c in client.get_collections().collections]
dbg.warning("collections AFTER create: %s", names_after)
if collection_name not in names_after:
raise RuntimeError(
f"Fixture created collection {collection_name} but it is not in "
f"get_collections(): {names_after}"
)
# Index test emails
for email in TEST_EMAILS:
vector_text = (
f"Date: {email['date']}\n"
f"From: {email['sender']}\n"
f"To: {email['receiver']}\n"
f"Subject: {email['subject']}\n\n"
f"{email['body_text']}\n\n"
f"Attachments: {', '.join(email['attachments']) if email['attachments'] else 'None'}"
)
vector = list(model.embed([vector_text]))[0].tolist()
# Create payload indexes on first upsert
client.create_payload_index(
collection_name=collection_name,
field_name="sender",
field_schema=models.PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
collection_name=collection_name,
field_name="receiver",
field_schema=models.PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
collection_name=collection_name,
field_name="date",
field_schema=models.PayloadSchemaType.DATETIME,
)
client.upsert(
collection_name=collection_name,
points=[
models.PointStruct(
id=str(uuid.uuid5(uuid.NAMESPACE_OID, email["message_id"])),
vector=vector,
payload={
"message_id": email["message_id"],
"date": email["date"],
"sender": email["sender"],
"sender_raw": email["sender_raw"],
"receiver": email["receiver"],
"receiver_raw": email["receiver_raw"],
"subject": email["subject"],
"body_text": email["body_text"],
"attachments": email["attachments"],
},
)
],
)
yield client
# Cleanup
try:
client.delete_collection(collection_name)
except Exception:
pass
class TestSearchEmails:
def test_search_by_content(self, qdrant_setup):
"""Semantic search returns the most relevant email first.
Searching for 'Python programming' should match email 001
which discusses Python and vector databases.
"""
result = search_emails(query="Python programming")
assert result["count"] >= 1
assert "results" in result
# The top result should be email 001 (most semantically relevant)
messages = [r["message_id"] for r in result["results"]]
assert "<test-001@example.com>" in messages
def test_search_with_participant_filter(self, qdrant_setup):
"""Search emails sent by alice."""
result = search_emails(query="help", participant="alice@example.com")
assert result["count"] >= 1
messages = [r["message_id"] for r in result["results"]]
assert "<test-001@example.com>" in messages
def test_search_with_date_filter(self, qdrant_setup):
"""Search emails after a specific date."""
result = search_emails(query="meeting", start_date="2026-02-01")
assert result["count"] >= 1
for r in result["results"]:
date = r.get("date", "")
assert date >= "2026-02-01"
def test_search_no_results_by_date(self, qdrant_setup):
"""Search with a filter that matches no emails returns empty results.
With semantic (vector) search, any text query always has nearest
neighbors, so we use a date filter that matches nothing instead.
"""
result = search_emails(
query="anything",
start_date="2099-01-01",
end_date="2099-12-31",
)
assert result["count"] == 0
assert result["results"] == []
class TestReadEmail:
def test_read_existing_email(self, qdrant_setup):
"""Read an email by its message_id."""
result = read_email(message_id="<test-001@example.com>")
assert "error" not in result
assert result["message_id"] == "<test-001@example.com>"
assert "This is a test email" in result.get("body_text", "")
def test_read_nonexistent_email(self, qdrant_setup):
"""Read an email that doesn't exist."""
result = read_email(message_id="<nonexistent@ghost.com>")
assert "error" in result
assert "No email found" in result["error"]
+163
View File
@@ -0,0 +1,163 @@
"""Unit tests for mcp-maildir server pure functions.
Env vars are set in tests/conftest.py (shared session-wide) — see the
conftest docstring for why test modules must not touch os.environ.
"""
from server import (
normalize_email_address,
payload_matches_participant,
format_search_result,
build_filter,
)
class TestNormalizeEmailAddress:
def test_empty_value(self):
assert normalize_email_address("") == ""
assert normalize_email_address(None) == ""
def test_simple_email(self):
assert normalize_email_address("user@example.com") == "user@example.com"
def test_display_name_with_email(self):
assert normalize_email_address("John Doe <john@example.com>") == "john@example.com"
def test_whitespace_and_case(self):
assert normalize_email_address(" USER@Example.COM ") == "user@example.com"
def test_invalid_email_fallback(self):
result = normalize_email_address("not-an-email")
# Returns stripped lowercase version of the input
assert result == "not-an-email"
class TestPayloadMatchesParticipant:
def test_sender_match_normalized(self):
payload = {"sender": "alice@example.com"}
assert payload_matches_participant(payload, "alice@example.com") is True
def test_receiver_match_normalized(self):
payload = {"receiver": "bob@example.com"}
assert payload_matches_participant(payload, "bob@example.com") is True
def test_sender_raw_match(self):
payload = {"sender_raw": "alice@other.com"}
assert payload_matches_participant(payload, "alice@other.com") is True
def test_no_match(self):
payload = {"sender": "alice@example.com", "receiver": "bob@example.com"}
assert payload_matches_participant(payload, "carol@example.com") is False
def test_display_name_in_sender(self):
payload = {"sender": "Alice <alice@example.com>"}
assert payload_matches_participant(payload, "alice@example.com") is True
def test_display_name_in_receiver(self):
payload = {"receiver": "Bob Smith <bob@example.com>"}
assert payload_matches_participant(payload, "bob@example.com") is True
def test_empty_payload(self):
assert payload_matches_participant({}, "someone@example.com") is False
def test_case_insensitive(self):
payload = {"sender": "ALICE@EXAMPLE.COM"}
assert payload_matches_participant(payload, "alice@example.com") is True
class TestFormatSearchResult:
def test_basic_formatting(self):
class MockPoint:
payload = {
"message_id": "<abc@def>",
"date": "2026-01-15T10:00:00",
"sender": "alice@example.com",
"receiver": "bob@example.com",
"subject": "Hello",
"attachments": ["file.pdf"],
}
score = 0.95
result = format_search_result(MockPoint())
assert result["message_id"] == "<abc@def>"
assert result["date"] == "2026-01-15T10:00:00"
assert result["sender"] == "alice@example.com"
assert result["receiver"] == "bob@example.com"
assert result["subject"] == "Hello"
assert result["attachments"] == ["file.pdf"]
assert result["score"] == 0.95
def test_empty_payload(self):
class MockPoint:
payload = None
score = None
result = format_search_result(MockPoint())
assert result["message_id"] is None
assert result["score"] is None
assert result["attachments"] == []
def test_missing_fields(self):
class MockPoint:
payload = {"message_id": "<123>"}
score = 0.5
result = format_search_result(MockPoint())
assert result["message_id"] == "<123>"
assert result["date"] is None
assert result["sender"] is None
assert result["receiver"] is None
assert result["subject"] is None
assert result["attachments"] == []
class TestBuildFilter:
def test_no_filters_returns_none(self):
assert build_filter(None, None, None) is None
def test_empty_strings_return_none(self):
assert build_filter("", "", "") is None
def test_participant_filter_is_should_clause_on_sender_and_receiver(self):
f = build_filter("alice@example.com", None, None)
assert f is not None
# The date conditions list must be absent and only the participant filter present
assert len(f.must) == 1
participant = f.must[0]
keys = [c.key for c in participant.should]
assert keys == ["sender", "receiver"]
assert all(c.match.value == "alice@example.com" for c in participant.should)
def test_participant_normalized_before_match(self):
f = build_filter("Alice <Alice@Example.COM>", None, None)
should = f.must[0].should
assert all(c.match.value == "alice@example.com" for c in should)
def test_date_range_only(self):
f = build_filter(None, "2026-01-01", "2026-06-30")
assert len(f.must) == 1
cond = f.must[0]
assert cond.key == "date"
assert cond.range.gte is not None
assert cond.range.lte is not None
def test_start_date_only(self):
f = build_filter(None, "2026-03-15", None)
cond = f.must[0]
assert cond.range.gte is not None
assert cond.range.lte is None
def test_end_date_only(self):
f = build_filter(None, None, "2026-03-15")
cond = f.must[0]
assert cond.range.gte is None
assert cond.range.lte is not None
def test_combined_participant_and_date(self):
f = build_filter("bob@example.org", "2026-01-01", "2026-12-31")
assert len(f.must) == 2
# One is the participant should-filter, the other the date range
kinds = sorted(
"participant" if getattr(c, "should", None) else "date" for c in f.must
)
assert kinds == ["date", "participant"]
+27
View File
@@ -0,0 +1,27 @@
"""Transport-level tests for the mcp-maildir ASGI app.
Current MCP clients (OpenCode, Claude, ...) only speak Streamable HTTP: they
POST JSON-RPC to the configured URL. Serving the legacy SSE transport alone
makes those clients fail with `405 Method Not Allowed`, so the app built by
`server.build_app()` must expose the MCP endpoint at /mcp.
Env vars are set in tests/conftest.py (shared session-wide).
"""
from server import build_app, MCP_HTTP_PATH
def app_paths() -> set:
return {getattr(route, "path", None) for route in build_app().routes}
class TestTransport:
def test_mcp_endpoint_is_streamable_http(self):
assert MCP_HTTP_PATH == "/mcp"
assert MCP_HTTP_PATH in app_paths()
def test_health_endpoint_is_exposed_for_probes(self):
assert "/health" in app_paths()
def test_legacy_sse_endpoint_is_gone(self):
assert "/sse" not in app_paths()