Commit Graph

49 Commits

Author SHA1 Message Date
bvandeusen 1feef179d2 ci(plugin): drop with: from the plugin job's checkout
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 3s
CI & Build / integration (push) Successful in 30s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 52s
CI & Build / Build & push image (push) Successful in 19s
Run 3027: the Plugin hooks job failed at checkout, before the script ran.
Adding a `with: fetch-depth: 0` block made actions/checkout@v6 fail to
extract on the act_runner —

  Cannot find module '/var/run/act/actions/<sha>/dist/index.js'

— while every bare `uses: actions/checkout@v6` in the same run succeeded.
The runner's action-cache handling is the difference, not git.

No depth was needed in the first place. The version check compares two
TREES, and a tree diff needs both trees, not a common ancestor. Verified
against a real depth-1 clone: after `git fetch --depth=1 origin
main:refs/remotes/origin/main`, both `git diff origin/main -- plugin` and
`git show origin/main:plugin/.claude-plugin/plugin.json` work. So the
explicit fetch already in the step is sufficient, and cheaper than the
full history the `with:` block was asking for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-28 21:00:14 -04:00
bvandeusen 51e5c22818 ci(plugin): gate plugin/** — the path that shipped two live defects
CI & Build / Plugin hooks (push) Failing after 2s
CI & Build / Python lint (push) Successful in 8s
CI & Build / integration (push) Successful in 31s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 20s
`plugin/` is not built into the image; installs fetch it from this repo via
.claude-plugin/marketplace.json, so a push IS the release. It was absent
from the workflow's `paths:` filter entirely, meaning plugin changes ran no
CI at all. Two separate defects reached a live install through that gap:

  #2198 — all four hook scripts inert (lowercase userConfig env vars,
          line-oriented `jq -rR`, line-oriented `cut -c`)
  #2209 — the fix for #2198 couldn't reach an install because the manifest
          version wasn't bumped, so the installer never refreshed its cache

Adds `plugin/**` + `.claude-plugin/**` to `paths:` and a `plugin` job
running scripts/check_plugin.py:

1. `bash -n` on every hook.
2. The three known-bad patterns from #2198. Verified by replay against
   c569cdd^ — all three are caught. Narrow by design; see below.
3. Shipped plugin content differs from origin/main => the manifest version
   must differ too. Stated against the base branch, not per-commit, so a
   batch needs one bump rather than one per commit. Replayed against
   c569cdd: correctly fails.

The checker found a real outstanding bug on its first run: the `cut -c1-2000`
prompt cap in scribe_autoinject.sh was still line-oriented. Only the
prior-art hook's copy got fixed in c569cdd. Now `head -c`. It then failed
on this very commit for a missing version bump, which is the third time
that rule has mattered and the first time something other than memory
enforced it. Manifest bumped to 0.1.20.

WHAT THIS DOESN'T COVER, and why. shellcheck is the right tool for check 2
and is NOT in ci-python; nor is jq, which every hook requires and silently
bails without — so a "runs and stays silent" smoke test would pass
vacuously today and prove nothing. Both need those two packages added to
the CI image in the CI-runner repo, which is a separate change to a
separate repo (rule #5: the toolchain comes from the image, not from
apt-get at job start). Verified against CI-runner's Dockerfile and
scripts/install-common.sh rather than assumed (rule #37).

`plugin` is not in the build job's `needs`: the plugin doesn't ship in the
image, and blocking the build wouldn't un-publish a bad hook — the push
already did. A failed job still reddens the run.

Closes #2204

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-28 20:56:15 -04:00
bvandeusen a47e1b9c4e fix(ci): regenerate uv.lock and enforce it with --locked
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 17s
Completes the previous two commits. `--frozen` failed run 3007 with
`ModuleNotFoundError: No module named 'pgvector'` — the lock wasn't merely
stale in its recorded metadata, it was missing a real dependency. pgvector
was added to pyproject for the vector-search work and the lock was never
regenerated, and nothing noticed because CI resolved from pyproject and never
read the lock. The lock has been dead weight for some time.

Regenerated with `uv lock` inside a throwaway `ci-python:3.14` container —
this workstation has no uv and no pip, and the CI image already carries the
right toolchain, so nothing was installed to do it. uv was conservative as
promised: pgvector 0.5.0 added, and NOT ONE existing pin moved (verified by
diffing name=version pairs across all 106 packages).

Both lanes now run `uv sync --locked`, so a dependency edit without a re-lock
fails loudly at install rather than resolving around the lock. The check paid
for itself on its first run by surfacing the missing pgvector.

Also added uv.lock to the workflow's `paths:` filter. It was absent, so a
lock-only change — exactly what a dependency bump looks like now — would not
have triggered CI at all.

Closes the CI half of #2194. The Dockerfile still resolves independently and
is tracked there.
2026-07-28 12:58:18 -04:00
bvandeusen 8bab0c762b fix(ci): use uv sync --frozen — --locked needs a lock this box can't regenerate
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Failing after 8s
CI & Build / Python tests (push) Failing after 21s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / Build & push image (push) Has been skipped
Run 3006 failed at the install step: "The lockfile at `uv.lock` needs to be
updated, but `--locked` was provided." The guard was working — the mcp cap
edited pyproject, so the lock genuinely is stale, and hand-editing the
recorded specifier wasn't enough to satisfy uv's freshness check.

Regenerating needs `uv lock`, and this workstation has neither uv nor pip
(rule #10 — local Python envs are deliberately absent), so obtaining it would
mean pulling a binary from github.com, against rule #3. Not doing that
unilaterally.

--frozen installs exactly what the lock pins and resolves nothing, which is
the whole point of #2194: no dependency can float into a run again. What it
gives up is only the staleness check — and a forgotten re-lock surfaces as a
loud ImportError, not as a silent version drift, so the failure mode is the
tolerable one.

Flip to --locked in the same change that runs `uv lock`. Refs #2194.
2026-07-28 12:55:06 -04:00
bvandeusen ef7ebddadf fix(ci): install from uv.lock so CI stops resolving dependencies itself
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Failing after 10s
CI & Build / Python tests (push) Failing after 16s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Build & push image (push) Has been skipped
Closes the reproducibility hole that turned `main` red an hour ago. CI ran
`uv pip install -e ".[dev]"`, which resolves from the pyproject constraints
and ignores uv.lock completely — so every dependency floated. uv.lock pinned
mcp 1.27.2; CI installed the 2.0.0 published mid-session and the identical
tree that passed on `dev` failed on `main`.

Both Python lanes now run `uv sync --locked --extra dev`. `--locked` also
fails when the lock is stale against pyproject, so a dependency change has to
go through a deliberate `uv lock` instead of arriving on its own — which also
restores the point of the Renovate dashboard-approval flow.

Dropped the http-ece install and the setuptools/wheel step that existed only
to support it: nothing in src/ or tests/ imports http_ece. It is a leftover
from the web-push subsystem removed in the MCP-First pivot, and it was never
in pyproject or uv.lock — CI was installing an unused package and carrying a
--no-build-isolation workaround for it.

Cache key moves from pyproject.toml to uv.lock, since the lock is now what
determines the installed set.

uv.lock's recorded root requirement updated to match the mcp cap. Edited by
hand rather than regenerated: uv isn't installed on this workstation, the
resolved mcp 1.27.2 already satisfies `<2`, so no re-resolution is needed —
only the staleness check needed satisfying.

The Dockerfile still resolves independently (`pip install .`, and it doesn't
even copy uv.lock), so the shipped image is not yet covered. Following
separately so a build break can't strand `main`. Refs #2194.
2026-07-28 12:50:52 -04:00
bvandeusen 513019786e feat(search): pgvector substrate — vector(384) + HNSW for semantic search
Move semantic_search_notes off the full-table Python cosine scan onto a native
pgvector column: indexed ORDER BY embedding <=> :q LIMIT k (HNSW, cosine).
Migration 0067 enables the extension, converts the JSONB embedding column to
vector(384) (stale-dim rows dropped and regenerated by the startup backfill),
and builds the HNSW cosine index. Postgres image moves postgres:16-alpine ->
pgvector/pgvector:pg17 across prod, quickstart, and CI.

Scribe: project 2, milestone 93, task 1031.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xz4j1H7pjYSjKsEpgcNH5E
2026-06-22 20:10:15 -04:00
bvandeusen 2ad2e943f3 test(ci): add Postgres integration lane + real run_maintenance guard
CI & Build / Python lint (push) Successful in 2s
CI & Build / integration (push) Failing after 19s
CI & Build / TypeScript typecheck (push) Successful in 20s
CI & Build / Python tests (push) Successful in 45s
CI & Build / Build & push image (push) Successful in 1m1s
The unit suite can't catch sync/async API mismatches against SQLAlchemy (an
un-awaited execution_options passed green CI but failed at runtime: VACUUM 0/6).
Add a real-Postgres integration lane modelled on the family pattern (rules
6/79-82): a new CI 'integration' job with a postgres:16 service, bridge-IP
discovery, busybox-safe readiness wait, and 'alembic upgrade head', running
pytest -m integration. Non-gating, like the unit lane.

- tests/test_integration_db_maintenance.py: runs run_maintenance() and
  get_table_health() against real Postgres; asserts all allowlisted tables
  vacuum OK (the await regression makes this fail) and health reports real stats.
- pyproject: register the 'integration' marker.
- conftest: integration-marked tests use the real DATABASE_URL, not the stub.
- ci.yml: unit 'test' job now runs -m 'not integration'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:19:18 -04:00
bvandeusen c972af2690 ci: make npm cache step non-fatal (fixes recurring typecheck flake #828)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 14s
The TypeScript-typecheck job intermittently failed at 'Cache npm download
cache' (transient cache-backend hiccup), which skipped install + type check and
marked the run red — 3x during the issues+systems build, all on pushes the
cache step had no bearing on. continue-on-error: true degrades a cache failure
to 'install without cache' instead of failing the job.

Closes the rerun churn from task #828.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:57:20 -04:00
bvandeusen 1d4c206563 ci: main publishes :latest (main is the production line)
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 18s
main pushes now move :latest (in addition to the immutable :<sha>), so a
merge to main updates production's pointer directly — no separate release
needed just to refresh :latest. The v* release tag's distinct job becomes
the dated :<version> marker (it still refreshes :latest harmlessly). Still
no :main tag. Rules 47/46 + 10/4 updated to match on both instances.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:27:53 -04:00
bvandeusen 9a0d5f3109 ci: drop the :main tag — main builds publish only the immutable :<sha>
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 44s
CI & Build / Build & push image (push) Successful in 14s
:latest (release-only) is the single production pointer; a :main moving
tag just duplicated it. main pushes still gate + build (the :<sha> image
is the rollback point), but no longer publish a :main alias. The tag was
new and unreferenced, so nothing depends on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:52:21 -04:00
bvandeusen 5a930319ba ci: gate and build main too (:main image); :latest stays release-only
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 45s
CI & Build / Build & push image (push) Successful in 15s
Previously main pushes were deliberately skipped — CI only ran on dev
and v* tags. This conflicted with the intended policy (CI on dev AND
main). Now main is a first-class gated, built line: dev->:dev, main->:main,
v* tag->:latest + :<version>, every build also tagged with the commit sha.
Per-ref concurrency already supersedes rapid pushes, so dev and main run
independently without stacking identical work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:27:09 -04:00
bvandeusen bccee7f192 fix(ci): use POSIX case for tag selection so :dev actually pushes
Buried smoking gun: every CI run since the ci-python:3.14 migration
has silently failed to push the `:dev` tag. The build logs for commit
2a374d9 show:

    /var/run/act/workflow/tags.sh: 4: [[: not found
    /var/run/act/workflow/tags.sh: 6: [[: not found

act_runner invokes the workflow's `run:` block with `sh -e` (dash on
Debian-based ci-python:3.14, NOT bash). The original bash-only `[[ ]]`
syntax failed silently, the `:dev` tag never got appended to TAGS,
and only the SHA-tagged image was pushed. The `:dev` tag in the
registry has been stuck on whatever build last managed to push it —
likely back when CI ran on a bash-y Ubuntu runner before the migration.

This is why the deployed stack has been running a stale image despite
multiple successful "CI passed" runs: it pulls `:dev`, and `:dev` was
months out of date.

POSIX `case` is dash-compatible AND bash-compatible. Same intent
(decide which extra tags to append based on ref); no behaviour change
other than actually executing correctly.

This commit itself touches .forgejo/workflows/ci.yml, so it triggers
a fresh CI run that — for the first time in a while — should push
both :<sha> AND :dev. After this lands, redeploying the stack will
finally pull the recent code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:20:46 -04:00
bvandeusen 2a374d9b86 ci: add workflow_dispatch for manual re-runs
Lets you re-run CI from the Forgejo Actions UI without needing a
trivial commit. Useful when:
- An image has been built but the deployed stack didn't pick it up
  (re-run forces a fresh push + any post-CI hooks fire again).
- A transient upstream issue caused a build to fail (HF download
  flake during the voice-bundle step, registry hiccup, etc.) and
  re-running against the same source produces different behaviour.

This commit itself touches .forgejo/workflows/ci.yml so it triggers
a build by the normal paths rule, giving you a fresh :dev image
right now in addition to enabling future manual re-runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:13:08 -04:00
bvandeusen 3f1bcc3360 ci: consume shared ci-python:3.14 image; bump runtime to 3.14
Migrate to the FabledRulebook CI-Runner contract:

- .forgejo/workflows/ci.yml: all four jobs (typecheck/lint/test/build)
  now schedule on the `python-ci` runner label and run inside
  container.image: git.fabledsword.com/bvandeusen/ci-python:3.14
  (Python 3.14 + Node 24 + ruff + uv + Docker CLI). Dropped the inline
  uv install in the test job — uv is now baked into the image.
- Dockerfile: production runtime bumped to python:3.14-slim so test
  results stay representative against what we ship.
- ci-requirements.md: new file at repo root declaring image deps and
  per-job installs (per FabledRulebook ci-runners.md).
- infra/Dockerfile.runner-base: deleted. The in-repo runner base
  (Ubuntu 24.04 + Python 3.12 + Node 22) is superseded by the shared
  ci-python image. The runner-host deployment files
  (runner-compose.yml + act-runner-config.yml) stay as deployment-shape
  documentation; source of truth is the deployed config.
- docs/development.md: CI/CD + Runner sections refreshed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:16:26 -04:00
bvandeusen e652dece9b refactor: rename Fabled Assistant to Fabled Scribe
Updates user-visible branding across frontend (PWA manifest, page title,
Settings, push fallback), backend (email templates and subjects, LLM
system prompt, CalDAV displayname, SMTP from-name default), README,
quickstart compose, and MCP server description.

Also updates the CI image path and quickstart image reference to
git.fabledsword.com/bvandeusen/fabledscribe in preparation for the
Forgejo repo rename. Internal Python package, env vars, and database
schema unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:28:25 -04:00
bvandeusen f12d29563a fix(briefing): 2-column layout with table-based weather forecast
Collapse briefing from 3-column to 2-column grid (chat + sidebar).
Weather moves to top of news column, forecast uses an HTML table so
rows align cleanly. Drop verbose condition text from forecast days
(emoji already conveys it). Switch CI node_modules cache to npm
download cache to avoid tar size error from onnxruntime-web.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 18:10:46 -04:00
bvandeusen 9157740069 ci: gate typecheck/lint/test on ref so main merge commits skip work
Forgejo Actions doesn't consistently honor `on.push.branches` as a
filter — the merge commit landing on main after a release PR was still
triggering the full CI workflow on a SHA that had already passed on
dev, burning ~1 minute of runner time for no reason.

The `build` job already had a ref guard, so no duplicate images were
being pushed, but typecheck/lint/test ran unconditionally. Add the
same guard (`refs/heads/dev` or `refs/tags/v*`) to those three so main
pushes trigger the workflow but every job skips immediately with zero
runner time.

Also tightened the build job's tag filter from `refs/tags/` to
`refs/tags/v` for consistency with the new guards and to avoid ever
building from a non-version tag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:40:41 -04:00
bvandeusen 717ac1b5d7 fix(ci): make registry cache export non-fatal
The Forgejo registry occasionally returns 400 on large cache layer
blob uploads, failing the entire build even though the image itself
pushed successfully. Adding ignore-error=true to cache-to so cache
failures don't block deployments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 11:36:27 -04:00
bvandeusen 5275be8588 fix(ci): install setuptools before http-ece for uv
uv creates bare venvs without setuptools (unlike pip). http-ece
doesn't declare setuptools as a build dependency but needs it when
built with --no-build-isolation.
2026-04-12 00:10:45 -04:00
bvandeusen 8d07b6c79e ci: new ci-runner base image + uv/ruff/node_modules cache
Runner base image (infra/Dockerfile.runner-base):
- Added uv (fast Python package installer, ~10x faster than pip)
- Added ruff (baked in, lint job drops from ~13s to ~2s)
- Added jq + tzdata
- Renamed tag from py3.12-node22 to ci-runner (purpose over contents)

Workflow (ci.yml):
- typecheck: cache node_modules directly instead of npm download cache;
  skip npm ci entirely on cache hits (9m41s → ~30s expected)
- lint: just `ruff check src/` — no venv, no install
- test: uv venv + uv pip install replaces pip (~10x faster resolves)
- all jobs: runs-on ci-runner label

Baseline: typecheck 9m41s, lint 13s, test 1m42s
2026-04-11 23:59:57 -04:00
bvandeusen 02138f5728 fix(ci): use venv for ruff install — pipx not on runner image
The py3.12-node22 runner doesn't ship pipx, so the previous commit's
pipx install ruff failed with command not found. Switched to the same
venv pattern the test job uses.
2026-04-11 16:31:36 -04:00
bvandeusen cc74901761 ci: add concurrency, registry cache, pipx ruff, least-privilege perms
- concurrency: cancel in-progress runs on new pushes (non-tag only);
  tag runs are never cancelled so releases can't kill themselves
- permissions: contents: read default, packages: write scoped to build
- docker build: explicit cache-from/cache-to against :cache tag so
  BuildKit warmth survives local runner cleanup
- lint: pipx install ruff instead of --break-system-packages
- actions/cache@v3 → @v4
- triggers main branch pushes for gate runs (build stays dev/tag only)
- comments added on the non-obvious bits (http-ece isolation, docker
  prune two-step, registry cache mode=max)
2026-04-11 16:11:27 -04:00
bvandeusen 3e42992f67 fix: stop CI from filling runner disk
Three sources of unbounded growth removed:
- Drop cache-from/cache-to registry: on a persistent self-hosted runner the
  local BuildKit layer cache already provides between-run reuse; the registry
  cache was redundant and pushed ~2 GB of torch layers on every build
- Switch docker system prune -f → -af so old :SHA-tagged images are removed,
  not just dangling ones (-f alone never touched named tags)
- Add docker builder prune --keep-storage 5g to bound the local BuildKit
  cache; pip mount cache (torch etc.) is recently-used so survives, stale
  intermediate layers are evicted first

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 13:44:09 -04:00
bvandeusen aec7a910f0 fix: disable provenance attestation for Forgejo registry compatibility
build-push-action@v7 generates OCI attestation manifests by default.
Forgejo's registry doesn't support OCI image index format with attestations,
causing the push to fail with "unknown". provenance: false disables this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 23:11:29 -04:00
bvandeusen 8140bc022c fix: resolve ENOSPC on CI by using BuildKit pip cache and registry layer cache
- Add `# syntax=docker/dockerfile:1` to enable BuildKit cache mounts
- Replace `--no-cache-dir` pip installs with `--mount=type=cache,target=/root/.cache/pip`
  so torch/CUDA wheels are reused across builds instead of re-downloaded every run
- Add `docker system prune -f` step before build to free dangling image/layer space
- Add `cache-from`/`cache-to` pointing to `:cache` tag so unchanged layers
  (including the heavy voice-deps layer) are pulled from registry instead of rebuilt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 16:23:59 -04:00
bvandeusen 553c38200a feat(fable-mcp): build wheel in Docker image, serve download, add admin log tool, and Settings install UI
- Dockerfile: build fable-mcp wheel into /app/dist/ during image build
- routes/fable_mcp_dist.py: GET /api/fable-mcp/info + /download endpoints
- app.py: register fable_mcp_dist_bp
- fable_mcp/tools/admin.py: get_app_logs() hitting /api/admin/logs
- fable_mcp/server.py: fable_get_app_logs MCP tool
- SettingsView: "Fable MCP" section in API Keys tab with download button and install instructions
- client.ts: getFableMcpInfo() helper
- ci.yml: add fable-mcp/** to trigger paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:26:05 -04:00
bvandeusen 00dee5ea0e ci: remove redundant pull_request trigger
All code goes through dev first where CI runs on push. PR runs
(dev→main) were duplicating already-validated checks and causing
queue contention. Build job was already gated to push-only events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 22:53:32 -04:00
Bryan Van Deusen 16b2b5c68e fix: remove registry build cache — Forgejo rejects large layer blobs
Even mode=min still hits the registry's blob size limit (400 Bad Request).
The local runner's Docker daemon layer cache is sufficient for fast
incremental builds without needing a separate registry cache tag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 12:53:02 -04:00
Bryan Van Deusen c232a7d079 fix: downgrade buildx cache export from mode=max to mode=min
mode=max exports all intermediate layer blobs which exceeded the Forgejo
registry's blob upload size limit (400 Bad Request). mode=min only exports
the final image layers, keeping cache entries small enough for the registry.
The actual image build and push was succeeding; only the cache write failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 12:32:06 -04:00
bvandeusen 247a9ab9f9 ci: don't run on main branch push — PR trigger covers pre-merge safety
Release is now cut by creating a Forgejo release on main with a v* tag;
the tag push event triggers CI + Docker build/push.
2026-03-12 00:03:50 -04:00
bvandeusen 9383f10dab ci: build only on dev push and release tag, not on main merge
Merging to main now runs CI checks only. The release tag (v*) is the
sole trigger for a production :latest image, eliminating the redundant
double-build that previously occurred on every PR merge + tag pair.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:13:18 -04:00
bvandeusen 4636c9a973 Add CalVer build-time version tracking
- Dockerfile: ARG BUILD_VERSION=dev → ENV APP_VERSION baked into image
- ci.yml: BUILD_VERSION passed as build-arg; set to git tag name on v*
  tag builds, "dev" on branch builds
- routes/api.py: GET /api/version returns {"version": APP_VERSION}
- SettingsView: fetches /api/version on mount, displays in About section
  under General tab

Version source of truth is the git tag (YY.MM.DD.N CalVer).
pyproject.toml / package.json versions are no longer maintained.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:10:57 -04:00
Bryan Van Deusen 5104fdf27a CI: fix ruff install on Ubuntu 24.04 (PEP 668)
Ubuntu 24.04 blocks pip install into the system Python by default.
Add --break-system-packages for the lint job's throwaway container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 11:21:35 -04:00
Bryan Van Deusen 94d818e519 CI: rename runner label ubuntu-latest → py3.12-node22
The label now explicitly declares the dependency versions it provides
rather than using the opaque ubuntu-latest alias. All runs-on fields
in ci.yml updated to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 11:04:35 -04:00
Bryan Van Deusen dc03d71898 CI: custom runner-base image, Node 22, pip caching
- infra/Dockerfile.runner-base: new Ubuntu 24.04 image with Python 3.12
  and Node 22 LTS pre-installed, tagged py3.12-node22. Eliminates the
  ~3-4 min deadsnakes PPA install that ran on every test job.
- infra/runner-compose.yml: update ubuntu-latest label to use
  runner-base:py3.12-node22 instead of node:20-bullseye.
- .forgejo/workflows/ci.yml: remove Python 3.12 install steps (now in
  base image), add actions/cache for pip keyed on pyproject.toml hash,
  bump node-version 20 → 22.
- Dockerfile: bump frontend build stage node:20-alpine → node:22-alpine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 10:45:59 -04:00
bvandeusen 4327b9f858 CI: use venv for Python tests to isolate setuptools from system Python
deadsnakes python3.12 sees system python3.10's setuptools at
/usr/lib/python3/dist-packages and skips installation, leaving python3.12
without setuptools. A venv gets its own site-packages so pip/setuptools
are properly present for http-ece's legacy setup.py build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:47:08 -04:00
bvandeusen 2b4163523b CI: pre-install http-ece with --no-build-isolation to fix PEP 517 isolation
pip builds legacy setup.py packages in an isolated environment that doesn't
inherit the globally installed setuptools. Pre-building http-ece with
--no-build-isolation bypasses that isolation so setuptools is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:34:12 -04:00
bvandeusen 52d14672e5 CI: install setuptools before dev deps to support legacy http-ece build
http-ece (pulled in by pywebpush) uses setup.py and requires setuptools,
which is not included in the deadsnakes Python 3.12 install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:29:05 -04:00
bvandeusen e34f6d6e54 Bump all GitHub Actions to latest major versions
actions/checkout v4 → v6
actions/setup-node v4 → v6  (Node 24 runtime, fixes punycode warning)
docker/setup-buildx-action v3 → v4  (Node 24 + ESM)
docker/login-action v3 → v4  (Node 24 + ESM)
docker/build-push-action v6 → v7  (Node 24 + ESM)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 21:41:14 -04:00
bvandeusen bb41ce48be Fix Python CI jobs: drop container, install Python 3.12 directly
python:3.12-slim has no Node, causing actions/checkout@v4's post-step
to fail with 'exec: node not found'. Drop the container: declaration
so jobs run on the ubuntu-latest runner (which has Node). Install
Python 3.12 via deadsnakes PPA for the test job since the runner base
image (Ubuntu 22.04) ships with Python 3.10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 21:33:11 -04:00
bvandeusen c6211e5239 Gate Docker build on CI passing
Merged ci.yml and build.yml into a single workflow. The build job now
declares needs: [typecheck, lint, test] so images are only pushed when
all checks are green. PRs run CI only; branch/tag pushes run CI then
build if successful.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 18:53:58 -04:00
bvandeusen 02df3a4be4 Add dev branch support to build workflow
dev push  → :dev + :<sha>
main push → :latest + :<sha>
tag push  → :latest + :<sha> + :<version>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 21:17:53 -04:00
bvandeusen fbd5d04f6f Add version tag support to build workflow
Tag pushes (v*) now push an additional versioned image tag alongside
:latest and :<sha>. To release: git tag v1.2.0 && git push origin v1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 21:04:50 -04:00
bvandeusen 65c17ab7e8 Fix Python jobs: use python:3.12-slim container instead of setup-python
actions/setup-python fails in act_runner because it tries to download
pre-built binaries from GitHub CDN which aren't available in this
environment. Using a Python container image directly is the correct
approach — same pattern as the Flutter jobs in fabled_app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:42:35 -04:00
bvandeusen ef3a4fafd4 Add path filters to CI and build workflows
Prevents workflows from running on changes to docs, README, infra,
summary.md, or other non-code files. Build workflow also skips on
test-only changes since tests don't affect the Docker image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:39:12 -04:00
bvandeusen feac66dcd0 Scope CI/CD to build+test only; clean up runner infra
Remove automated deploy — Portainer handles deployments for now.
build.yml: deploy job removed, only builds and pushes image to registry.
infra/runner-compose.yml: clean standalone Portainer stack for act_runner,
  no swarm network dependency, uses external Forgejo URL.
infra/runner-swarm.yml: removed (replaced by runner-compose.yml).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:46:46 -04:00
bvandeusen 93fcd753a6 Simplify CI/CD for same-swarm topology: no SSH, direct service update
Since Forgejo and the app share a swarm, the runner can:
- Connect to Forgejo at http://forgejo:3000 (internal, no Traefik)
- Call docker service update directly via mounted socket
- Avoid SSH entirely — secrets drop from 6 to 3

infra/runner-swarm.yml: runner added to Forgejo stack, pinned to manager
  node (required for service API access), uses Docker Swarm Config object
  for act_runner config injection
infra/act-runner-config.yml: runner config (bridge network for jobs,
  docker socket whitelisted)
build.yml: deploy step replaced with docker service update --with-registry-auth
  --detach=false (waits for rollout, shows progress in Actions log)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:06:11 -04:00
bvandeusen 1dc771cec2 Fix CI/CD for Forgejo-on-swarm topology; add runner swarm compose
- build.yml: clarify that runner needs Docker socket; SSH deploy uses
  DEPLOY_PATH secret so app host path is configurable; --no-deps so
  db/ollama containers are left untouched during deploy
- infra/runner-swarm.yml: act_runner as a Docker service on the Forgejo
  host — mounts host socket for buildx, writes act_runner config via
  one-shot init container, connects to fabled_backend network

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 18:55:40 -04:00
bvandeusen e128406790 Add Forgejo Actions CI/CD, ruff linting, and test foundation
- .forgejo/workflows/ci.yml: fast checks on every push (TS typecheck,
  Python lint via ruff, pytest) — no Docker build, ~30s with cache
- .forgejo/workflows/build.yml: Docker build with registry-side layer
  caching + SSH deploy on main branch pushes only
- pyproject.toml: add ruff to dev deps, configure pytest and ruff rules
- tests/: conftest.py + first unit tests for _safe_filename (no DB needed)
- Makefile: add lint, fmt, typecheck, test, check targets for local use

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 18:46:54 -04:00