Compare commits

..
Author SHA1 Message Date
bvandeusen 6c76f08b69 Merge pull request 'Per-artifact commit-derived versions: pinnable date tags, and no rebuild when nothing changed' (#238) from dev into main
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / build-ml (push) Successful in 5s
Build images / build-agent (push) Successful in 6s
Build images / build-web (push) Successful in 6s
extension / lint (push) Successful in 17s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m42s
2026-08-28 10:14:15 -04:00
bvandeusen 7b1019ba82 Merge pull request 'Extension channels: dev and main each carry their own signed extension' (#237) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 28s
extension / lint (push) Successful in 23s
CI / extension-version (push) Successful in 3s
Build images / build-web (push) Successful in 2m21s
Build images / build-ml (push) Successful in 3m12s
CI / integration (push) Successful in 5m29s
Build images / build-agent (push) Successful in 9m54s
2026-08-27 12:49:39 -04:00
bvandeusen 0d204e6837 Merge pull request 'Extension packaging: one source of truth, shadow-mode versioning, real XPI verification' (#235) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 3s
CI / frontend-build (push) Successful in 34s
extension / lint (push) Successful in 40s
CI / backend-lint-and-test (push) Successful in 2m1s
CI / integration (push) Successful in 4m3s
Build images / build-web (push) Successful in 4m21s
Build images / build-ml (push) Successful in 5m39s
Build images / build-agent (push) Successful in 12m49s
2026-08-03 20:23:27 -04:00
bvandeusen 8300029741 Merge pull request 'Extension: fix credential-push 405, guard the publish path, add a unit suite' (#234) from dev into main
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 32s
extension / lint (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 1m14s
Build images / sign-extension (push) Successful in 2m23s
Build images / build-ml (push) Successful in 3m53s
CI / integration (push) Successful in 3m56s
Build images / build-web (push) Successful in 2m34s
Build images / build-agent (push) Successful in 13m33s
2026-08-03 08:29:09 -04:00
bvandeusen b5b437ca80 Merge pull request 'feat(agent): idle-unload GPU models to free VRAM when the queue is idle' (#233) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 34s
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 25s
Build images / build-web (push) Successful in 3m20s
Build images / build-ml (push) Successful in 4m29s
CI / integration (push) Successful in 4m26s
Build images / build-agent (push) Successful in 11m55s
2026-07-17 13:07:15 -04:00
bvandeusen ce0dac3524 Merge pull request 'DRY + stability pass — extension, ML/settings backend, frontend cards (#161)' (#232) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 12s
extension / lint (push) Successful in 11s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 50s
CI / integration (push) Successful in 3m47s
2026-07-13 23:08:18 -04:00
bvandeusen 9b5ec86222 Merge pull request 'feat(extension): in-app update prompt + v1.0.9 (#1489)' (#231) from dev into main
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
extension / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 30s
Build images / sign-extension (push) Successful in 3m34s
Build images / build-web (push) Successful in 8s
CI / integration (push) Successful in 3m50s
2026-07-13 18:32:31 -04:00
bvandeusen 89c83ee5de Merge pull request 'fix(extension): Add-to-FC button on subscribed Patreon creators + v1.0.8 (#1485)' (#230) from dev into main
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
extension / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m46s
Build images / sign-extension (push) Successful in 5m8s
Build images / build-web (push) Successful in 10s
2026-07-13 17:56:32 -04:00
bvandeusen d3192f1843 Merge pull request 'chore(deps): web-ext 8→10 (Renovate batch 5) (#1450)' (#229) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
extension / lint (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m48s
2026-07-13 16:36:26 -04:00
bvandeusen 5a5694f200 Merge pull request 'fix(ui): softer chrome-gradient falloff + segmented media toggle (#1478)' (#228) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m42s
2026-07-13 15:55:02 -04:00
bvandeusen bb1a938cc0 Merge pull request 'fix(ui): measure real nav height (--fc-nav-h) — Explore fits on 1080p, no breadcrumb tuck (#1481)' (#227) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m50s
2026-07-13 15:32:25 -04:00
bvandeusen fc0293029d Merge pull request 'fix(ui): Vuetify-4 review polish — active-tab indicator + one continuous chrome gradient' (#226) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m44s
2026-07-13 14:47:59 -04:00
bvandeusen 7d1c701b67 Merge pull request 'fix(ui): remove redundant app-shell top gap (#1480)' (#225) from dev into main
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m48s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 7s
2026-07-13 14:12:41 -04:00
bvandeusen b638382cd5 Merge pull request 'Ship: UI framework batch 4 — Vuetify 4 + vue-router 5 + pinia 3 (#1449)' (#224) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 28s
CI / frontend-build (push) Successful in 34s
CI / backend-lint-and-test (push) Successful in 53s
CI / integration (push) Successful in 3m46s
2026-07-13 13:40:25 -04:00
bvandeusen 17903068b4 Merge pull request 'Ship: system-tag refactor + soft WIP tier + Explore reach (#1464, #1474, #1476)' (#223) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 9s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m45s
2026-07-13 12:13:19 -04:00
bvandeusen 31d400ab0a Merge pull request 'Ship: title-based WIP auto-tagging (#1458)' (#222) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m50s
2026-07-12 19:45:03 -04:00
bvandeusen d45ce5e426 Merge pull request 'Ship: Renovate onboarding + dependency modernization batches 1–3' (#221) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 8s
extension / lint (push) Successful in 16s
CI / frontend-build (push) Successful in 29s
CI / backend-lint-and-test (push) Successful in 1m4s
Build images / build-web (push) Successful in 1m24s
Build images / build-ml (push) Successful in 2m36s
CI / integration (push) Successful in 3m46s
2026-07-11 23:16:33 -04:00
bvandeusen 77bfc32b02 Merge pull request 'feat(translation): tunable acceptance floor (0.90) + per-post sticky override (#155)' (#210) from dev into main
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 45s
CI / integration (push) Successful in 3m45s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 8s
2026-07-10 22:47:45 -04:00
bvandeusen d13efe1b69 Merge pull request 'refactor(tags): unify suggestion source — one canonical DB-tag dropdown (#154)' (#209) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 13s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 41s
CI / integration (push) Successful in 3m44s
2026-07-10 15:52:44 -04:00
bvandeusen ca6d26d236 Merge pull request 'feat(translation): acceptance gate on Interpreter detection confidence (#1376)' (#208) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 29s
CI / integration (push) Successful in 3m43s
2026-07-09 17:58:22 -04:00
bvandeusen 5366047d55 Merge pull request 'feat(translation): 8h sweep + drain button + detection probe (#1376)' (#207) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m50s
2026-07-08 20:19:33 -04:00
bvandeusen 2923257529 Merge pull request 'fix(ui): tag-chip left-edge clipping + kind-coloured border' (#206) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m45s
2026-07-08 10:37:09 -04:00
bvandeusen 934731e9ef Merge pull request 'feat(translation): per-field language detection for mixed-language posts' (#205) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m48s
2026-07-08 09:25:23 -04:00
bvandeusen 4c67c116b2 Merge pull request 'Bolder auto-tag accept/reject + larger tag chips' (#204) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m50s
2026-07-07 23:21:26 -04:00
bvandeusen f8b667604f Merge pull request 'Graceful shutdown + resilience & Explore polish (m147)' (#203) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 33s
CI / integration (push) Successful in 3m48s
2026-07-07 22:53:34 -04:00
bvandeusen 11572f469a Merge pull request 'Re-translate on Interpreter model change — artist-scoped + global (m146)' (#202) from dev into main
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m41s
2026-07-07 20:22:40 -04:00
bvandeusen ba3fd2a118 Merge pull request 'Post translation via Interpreter (milestone 143)' (#201) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m44s
2026-07-07 13:05:18 -04:00
bvandeusen 9bf095179a Merge pull request 'Auto-tag accept/reject as an in-pill yes/no pair; confirm refocuses the tag input' (#200) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m44s
2026-07-07 01:13:02 -04:00
bvandeusen cefc064e0f Merge pull request 'Surface auto-hide misfires proactively (review strip no longer gated on Show-hidden)' (#199) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 31s
CI / integration (push) Successful in 3m42s
2026-07-07 00:43:45 -04:00
bvandeusen 06757daf80 Merge pull request 'Presentation-chrome auto-hide (milestone 141) + tagging-UX fixes' (#198) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m46s
2026-07-07 00:24:51 -04:00
bvandeusen a723ef436b Merge pull request 'Soft auto-apply (retract + confirm, no self-training) + tagging UX (reject-rest, tag-input race, modal playlist)' (#197) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 9s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 37s
CI / integration (push) Successful in 3m46s
2026-07-06 21:03:30 -04:00
bvandeusen d68f39ca50 Merge pull request 'Suggestions perf + UX polish: incremental CCIP + head training, hover label, scroll/toast fixes, dead-preview removal' (#196) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 31s
CI / integration (push) Successful in 3m43s
2026-07-06 16:41:08 -04:00
bvandeusen a66a695977 Merge pull request 'Tag-hover → crop-region grounding overlay (#1206, milestone #133)' (#195) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 17s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 40s
CI / integration (push) Successful in 3m51s
2026-07-06 13:29:31 -04:00
bvandeusen 5289fa3879 Merge pull request 'Crop-proposer detector config → Settings → Tagging (default-on, lease-announced) (#134)' (#194) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m40s
2026-07-05 20:10:02 -04:00
bvandeusen ebac34e17b Merge pull request 'Artist identity/storage/display decoupling — rename, move-source, add-time name resolution (#130)' (#193) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m40s
2026-07-04 22:43:12 -04:00
bvandeusen 4c3ba20198 Merge pull request 'Recapture image→post linking (#1288) + ugoira timings + system suggestion category' (#192) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 41s
CI / integration (push) Successful in 3m36s
2026-07-04 20:36:58 -04:00
bvandeusen d2acec61ae Merge pull request 'Pixiv native ingester (#129) + SubscribeStar .art fix, disable-clears-failure, system-tag suggestion floor' (#191) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 39s
CI / integration (push) Successful in 3m35s
2026-07-03 16:36:56 -04:00
bvandeusen 15ae5ef4fa Merge pull request 'SubscribeStar .art age fix, modal tag-flow sync, system tags / training hygiene (#128)' (#190) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 9s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m34s
2026-07-03 09:16:16 -04:00
bvandeusen e3ceefc820 Merge pull request 'Surgical re-fetch for deep posts (ExternalLink reset) — recovery no longer needs deep scans' (#189) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 9s
Build images / build-agent (push) Successful in 14s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 38s
CI / backend-lint-and-test (push) Successful in 2m10s
CI / integration (push) Successful in 3m40s
2026-07-02 21:14:45 -04:00
bvandeusen e52090a4cf Merge pull request 'Agent sleep mode: empty queue sheds to one downloader, lease poll backs off to 15 min' (#188) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
CI / frontend-build (push) Successful in 21s
Build images / build-web (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 42s
CI / integration (push) Successful in 3m50s
2026-07-02 20:37:48 -04:00
bvandeusen bfba8045e4 Merge pull request 'Admin IA overhaul, cap-aware autoscaler, optional ml-worker, hardened tag reset' (#187) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 40s
CI / integration (push) Successful in 3m29s
2026-07-02 18:01:56 -04:00
bvandeusen a708357436 Merge pull request 'Settings defaults, GPU error tombstones + failure triage/recovery, agent bandwidth cap, approved retirements' (#186) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 9s
Build images / build-ml (push) Successful in 10s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 38s
CI / integration (push) Successful in 3m33s
2026-07-02 15:48:48 -04:00
bvandeusen d9354ac1e1 Merge pull request 'Agent: short-video sampling fix + "Retry errored jobs" recovery action' (#185) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m26s
2026-07-01 21:13:48 -04:00
bvandeusen 1d48770793 Merge pull request 'Agent: fix Status freeze — conchint.textContent destroyed #capn (root cause)' (#184) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m25s
2026-07-01 20:46:28 -04:00
bvandeusen 489e6aaaee Merge pull request 'Agent: server-side throughput rates + killable-on-stop ffmpeg' (#183) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 9s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m26s
2026-07-01 20:01:00 -04:00
bvandeusen ed20df905b Merge pull request 'Agent: DRY pass + Status rate-metrics + real start/stop state machine' (#182) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m25s
2026-07-01 16:30:32 -04:00
bvandeusen 5ba9871ef0 Merge pull request 'fix(agent): stream videos via ffmpeg-from-URL (no full download) — env-agnostic' (#181) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m27s
2026-07-01 14:20:53 -04:00
bvandeusen 2a820d0848 Merge pull request 'fix(agent): log the real fetch/submit failure reason (diagnose #1225)' (#180) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m28s
2026-07-01 12:37:45 -04:00
bvandeusen 2f9aa3d86c Merge pull request 'perf(web): 4 MiB file streaming + 4 hypercorn workers (fix 40s downloads)' (#179) from dev into main
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 29s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / integration (push) Successful in 3m26s
2026-07-01 11:55:35 -04:00
bvandeusen 560a5000a2 Merge pull request 'gallery: sort by earliest post date across all posts (new default)' (#178) from dev into main
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m26s
2026-07-01 10:54:41 -04:00
bvandeusen 7ddad231f8 Merge pull request 'agent: stop downloader pool stampeding a slow curator (congestion collapse)' (#177) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 7s
CI / integration (push) Successful in 3m22s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 35s
2026-07-01 10:45:48 -04:00
bvandeusen a78f7eaace Merge pull request 'explore: more variance in the related rail (stronger MMR diversification)' (#176) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m25s
2026-07-01 00:50:59 -04:00
bvandeusen 71337b0ba4 Merge pull request 'agent: temporal video dedup — drop near-duplicate frames before the GPU' (#175) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m25s
2026-07-01 00:38:53 -04:00
bvandeusen 3996205f3b Merge pull request 'agent: dedupe near-duplicate crops before the SigLIP embed' (#174) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 5s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m26s
2026-07-01 00:24:39 -04:00
bvandeusen 9f9db01456 Merge pull request 'agent: download/GPU producer-consumer pipeline + detector fuse fix' (#173) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m25s
2026-06-30 23:39:25 -04:00
bvandeusen 216b7fc743 Merge pull request 'Agent: bound video GPU work (early-exit frame loop at max_regions)' (#172) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m26s
2026-06-30 22:57:33 -04:00
bvandeusen fbb76e6f36 Merge pull request 'Agent: huge-image load + figure/region caps + stale-active fix' (#171) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m25s
2026-06-30 22:47:48 -04:00
bvandeusen 5ef1478ade Merge pull request 'Modernise agent base → Ubuntu 24.04 / Python 3.12 / CUDA 12.9' (#170) from dev into main
Build images / build-ml (push) Failing after 3s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 41s
CI / integration (push) Successful in 3m22s
Build images / build-agent (push) Successful in 8m44s
2026-06-30 22:30:18 -04:00
bvandeusen 88ff4147e1 Merge pull request 'Fix: agent py3.10 startup crash + submit-path retry + pin agent ruff to py310' (#169) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m25s
2026-06-30 22:03:57 -04:00
bvandeusen 1ea02ad44c Merge pull request 'Release: prompt agent stop + lazy curator polling + build marker + agent in CI' (#168) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-web (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m23s
2026-06-30 21:42:55 -04:00
bvandeusen 937cfb65b4 Merge pull request 'Release: agent per-stage timing breakdown' (#167) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
CI / backend-lint-and-test (push) Successful in 25s
Build images / build-web (push) Successful in 5s
CI / frontend-build (push) Successful in 18s
CI / integration (push) Successful in 3m23s
2026-06-30 21:28:57 -04:00
bvandeusen baac851220 Merge pull request 'perf: GPU-job leasing stays O(batch) — partial indexes + two-phase lease' (#166) from dev into main
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 9s
Build images / build-ml (push) Successful in 9s
CI / integration (push) Successful in 3m27s
CI / lint (push) Successful in 4s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
2026-06-30 21:18:23 -04:00
bvandeusen a28c33281a Merge pull request 'Release: unfreeze agent status view + smoothed throughput autoscaler + log pane' (#165) from dev into main
CI / lint (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 9s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 16s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m23s
2026-06-30 20:20:34 -04:00
bvandeusen 40be0a9323 Merge pull request 'Release: agent page no-store cache header' (#164) from dev into main
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Failing after 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 16s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m23s
2026-06-30 20:00:36 -04:00
bvandeusen 2c6bf26bfc Merge pull request 'Release: graceful Start/Stop (starting/stopping states) + cached GPU reads' (#163) from dev into main
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 16s
CI / backend-lint-and-test (push) Successful in 26s
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
CI / integration (push) Successful in 3m24s
2026-06-30 19:38:50 -04:00
bvandeusen 3c1e76bd44 Merge pull request 'Release: stable util-band autoscaler + live GPU meters' (#162) from dev into main
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 5s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 16s
Build images / build-web (push) Successful in 6s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m44s
2026-06-30 19:16:29 -04:00
bvandeusen 831c5b1c10 Merge pull request 'Release: agent page — centered column + logs fill to bottom' (#161) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m25s
2026-06-30 19:10:50 -04:00
bvandeusen 76b6af4903 Merge pull request 'Release: agent perf (batched crop embeds) + UI (full-width, copy logs, quiet noise) + truncated-image fix' (#160) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
CI / frontend-build (push) Successful in 17s
Build images / build-web (push) Successful in 6s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m32s
2026-06-30 18:47:55 -04:00
bvandeusen 92494ec4ed Merge pull request 'Release: agent control-page logs console + styling pass' (#159) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 7s
CI / integration (push) Successful in 3m26s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
2026-06-30 18:37:35 -04:00
bvandeusen bdfc17477c Merge pull request 'Release: GPU agent autoscaler (throughput hill-climb, Auto default-on)' (#158) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m24s
2026-06-30 18:23:15 -04:00
bvandeusen 6cd3153bf4 Merge pull request 'Release: SigLIP 2 default (new installs) + embedder model dropdown' (#157) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m26s
2026-06-30 18:01:01 -04:00
bvandeusen 5f2853168a Merge pull request 'Release: GPU re-process trigger (apply new crop detectors to the existing library)' (#156) from dev into main
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m22s
Build images / build-agent (push) Successful in 3m35s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
2026-06-30 16:13:36 -04:00
bvandeusen 9a979ee808 Merge pull request 'Release: Explore diversification, model-swap, Camie/centroid retirement, tag-API, crop proposers' (#155) from dev into main
Build images / build-agent (push) Successful in 10m56s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 29s
Build images / build-web (push) Successful in 2m21s
Build images / build-ml (push) Successful in 2m42s
CI / integration (push) Successful in 3m26s
2026-06-30 16:06:13 -04:00
bvandeusen 3138f912fd Merge pull request 'Release: SigLIP concept crops + max-over-bag + agent redeploy-survival' (#154) from dev into main
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
Build images / sign-extension (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 26s
Build images / build-web (push) Successful in 2m18s
Build images / build-ml (push) Successful in 2m44s
CI / integration (push) Successful in 3m35s
Build images / build-agent (push) Successful in 7m50s
2026-06-30 08:46:20 -04:00
bvandeusen 9df874e396 Merge pull request 'CCIP automation + reference quality (auto-enqueue, auto-apply, contamination fix, cache)' (#152) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 28s
CI / frontend-build (push) Successful in 20s
Build images / build-web (push) Successful in 2m20s
Build images / build-ml (push) Successful in 2m40s
Build images / build-agent (push) Successful in 3m17s
CI / integration (push) Successful in 3m27s
2026-06-29 22:29:30 -04:00
bvandeusen 6915a7590a Merge pull request 'fix(tags): refocus tag input after reject/un-reject too' (#151) from dev into main
CI / integration (push) Successful in 3m26s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 16s
CI / backend-lint-and-test (push) Successful in 27s
2026-06-29 21:07:21 -04:00
bvandeusen 5e8c28236a Merge pull request 'CCIP match threshold (tunable, 0.85) + Explore → variance' (#150) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m29s
2026-06-29 20:48:14 -04:00
bvandeusen 7e3c0f0b74 Merge pull request 'agent: raise worker cap to 32 + size HTTP pool' (#149) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 1s
CI / backend-lint-and-test (push) Successful in 26s
CI / frontend-build (push) Successful in 20s
Build images / build-web (push) Successful in 2m17s
Build images / build-ml (push) Successful in 2m47s
Build images / build-agent (push) Successful in 3m11s
CI / integration (push) Successful in 3m31s
2026-06-29 19:42:11 -04:00
bvandeusen 5d0c7ba706 Merge pull request 'fix(agent): flatten transparency onto white before RGB' (#148) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 6s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m27s
2026-06-29 19:18:48 -04:00
bvandeusen 18300e1f8a Merge pull request 'Agent: GPU load + live worker tuning + fast orphan recovery' (#147) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 29s
CI / integration (push) Successful in 3m27s
2026-06-29 19:11:22 -04:00
bvandeusen d52ac0a0e2 Merge pull request 'fix(agent): cuDNN base image so onnxruntime-gpu loads' (#146) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 16s
CI / backend-lint-and-test (push) Successful in 26s
Build images / build-web (push) Successful in 2m19s
Build images / build-ml (push) Successful in 2m50s
Build images / build-agent (push) Successful in 3m29s
CI / integration (push) Successful in 3m28s
2026-06-29 18:48:24 -04:00
bvandeusen 401fe8213e Merge pull request 'ci: publish the GPU agent image' (#145) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-agent (push) Successful in 3m15s
CI / integration (push) Successful in 3m27s
2026-06-29 14:26:51 -04:00
bvandeusen e8774d7953 Merge pull request 'CCIP characters + crop/region pipeline + desktop GPU agent (#114)' (#144) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 26s
CI / lint (push) Successful in 3s
Build images / build-web (push) Successful in 2m8s
Build images / build-ml (push) Successful in 2m37s
CI / integration (push) Successful in 3m26s
2026-06-29 14:18:57 -04:00
bvandeusen bc0f00c51b Merge pull request 'Earned auto-apply (fire + observability + UI), retrain cadences, Explore arrow-nav' (#143) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-web (push) Successful in 2m2s
Build images / build-ml (push) Successful in 2m30s
CI / integration (push) Successful in 3m24s
2026-06-29 07:30:41 -04:00
bvandeusen 1bef68aa29 Merge pull request 'Tagging-v2: heads are the suggestion source (learn-from-tags) + rail accept/reject' (#142) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / frontend-build (push) Successful in 18s
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 25s
Build images / build-web (push) Successful in 2m4s
Build images / build-ml (push) Successful in 2m44s
CI / integration (push) Successful in 3m25s
2026-06-28 11:55:49 -04:00
bvandeusen 1a4bc2f981 Merge pull request 'tag-eval: "keep" records a confirmation so doubts stop resurfacing' (#141) from dev into main
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-web (push) Successful in 2m5s
CI / integration (push) Successful in 3m20s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 2m30s
2026-06-28 01:36:26 -04:00
bvandeusen 862ace69d6 Merge pull request 'fix(tag-eval): stop re-suggesting already-rejected items' (#140) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m19s
2026-06-28 01:10:26 -04:00
bvandeusen abf88b1a15 Merge pull request 'tag-eval: auto-apply operating point + server-side top-N concept discovery' (#139) from dev into main
CI / backend-lint-and-test (push) Failing after 2s
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 20s
Build images / build-web (push) Successful in 2m3s
Build images / build-ml (push) Successful in 2m37s
CI / integration (push) Successful in 3m19s
2026-06-28 00:54:14 -04:00
bvandeusen c05dcafbea Merge pull request 'fix(tag-eval): example thumbnail opens the view modal instead of Explore' (#138) from dev into main
Build images / build-ml (push) Successful in 7s
CI / frontend-build (push) Successful in 18s
CI / integration (push) Successful in 3m20s
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-web (push) Successful in 9s
CI / backend-lint-and-test (push) Successful in 27s
2026-06-28 00:11:20 -04:00
bvandeusen 55e8632dab Merge pull request 'Tag-eval review actions: bigger clickable thumbnails + inline confirm/reject' (#137) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-web (push) Successful in 2m2s
Build images / build-ml (push) Successful in 2m36s
CI / integration (push) Successful in 3m18s
2026-06-27 23:49:12 -04:00
bvandeusen 825e6b90bf Merge pull request 'Tag-eval (heads vs centroid) + focus/provenance/layout/sticky-header fixes + tag-gaps cleanup' (#136) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-web (push) Successful in 2m5s
Build images / build-ml (push) Successful in 2m31s
CI / integration (push) Successful in 3m21s
2026-06-27 23:03:04 -04:00
bvandeusen fb012c557c Merge pull request 'fix(explore): bound 3-pane grid row so a tall rail can't scroll the page' (#135) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-web (push) Successful in 2m5s
Build images / build-ml (push) Successful in 2m37s
CI / integration (push) Successful in 3m20s
2026-06-26 23:37:43 -04:00
bvandeusen 66593ab895 Merge pull request 'Explore: focus-everywhere + provenance in rail; tag-gaps cleanup' (#134) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m17s
2026-06-26 21:40:56 -04:00
bvandeusen b266a54ad3 Merge pull request 'feat(explore): auto-focus tag input on every image change' (#133) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m25s
2026-06-26 11:28:32 -04:00
bvandeusen ad803b646f Merge pull request 'fix(modal): place meta + save block under Provenance, above Tags' (#132) from dev into main
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m20s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 11s
2026-06-26 10:14:15 -04:00
bvandeusen 1f5da3d283 Merge pull request 'Fandom count fix + Explore 3-pane workspace + modal rail layout' (#131) from dev into main
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m21s
2026-06-26 08:24:26 -04:00
bvandeusen 93034f580d Merge pull request 'fix(tags): fandom views aggregate images via their characters' (#130) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-web (push) Successful in 2m6s
Build images / build-ml (push) Successful in 2m34s
CI / integration (push) Successful in 3m19s
2026-06-26 00:28:21 -04:00
bvandeusen 9b9b12f410 Merge pull request 'Modal suggestions scroll-cap + Celery broker resilience' (#129) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-web (push) Successful in 2m3s
Build images / build-ml (push) Failing after 2m34s
CI / integration (push) Successful in 3m30s
2026-06-24 08:34:17 -04:00
bvandeusen 376d310693 Merge pull request 'Tagging & viewing roadmap: tag query surface + hygiene projections + Explore view (Clusters A/B/C)' (#128) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-web (push) Successful in 2m6s
Build images / build-ml (push) Successful in 2m39s
CI / integration (push) Successful in 3m24s
2026-06-23 08:57:34 -04:00
bvandeusen bc69495a16 Merge pull request 'Provenance archive linkage, post reconciliation close-out, and the cleanup/admin DRY pass' (#127) from dev into main
Build images / build-web (push) Successful in 2m2s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-ml (push) Successful in 2m33s
CI / integration (push) Successful in 3m22s
2026-06-22 20:22:56 -04:00
bvandeusen 478f898e72 Merge pull request 'feat(settings): Maintenance tab compact tiles + centered Settings (pass 2)' (#126) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 37s
Build images / build-web (push) Successful in 2m18s
Build images / build-ml (push) Successful in 3m3s
CI / integration (push) Successful in 3m26s
2026-06-18 00:11:54 -04:00
bvandeusen 38a5e7f332 Merge pull request 'feat(settings): tidy Cleanup tab into sectioned compact tiles (pass 1)' (#125) from dev into main
CI / backend-lint-and-test (push) Failing after 2s
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 19s
CI / integration (push) Successful in 3m19s
2026-06-17 23:49:21 -04:00
bvandeusen 57fe15c267 Merge pull request 'feat(maintenance): reconcile duplicate posts (gallery-dl→native unify)' (#124) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 9s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m22s
2026-06-17 22:01:12 -04:00
bvandeusen eb3231ef10 Merge pull request 'fix(subscribestar): port gallery-dl date extraction (wrapped dates) + parse canary' (#123) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m19s
2026-06-17 21:15:43 -04:00
bvandeusen e9af459c0d Merge pull request 'feat(subscribestar): port gallery-dl doc + audio attachment extraction' (#122) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 9s
CI / frontend-build (push) Successful in 17s
Build images / build-web (push) Successful in 7s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m15s
2026-06-17 16:20:12 -04:00
bvandeusen 6f02806aec Merge pull request 'fix(subscribestar): port gallery-dl content + preview-skip extraction (body bug)' (#121) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 12s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m25s
2026-06-17 16:10:56 -04:00
bvandeusen a1d19bd96a Merge pull request 'fix(subscribestar): mirror gallery-dl's full request profile (verify_subscriber gate)' (#120) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m24s
2026-06-17 15:32:28 -04:00
bvandeusen 26827ff38f Merge pull request 'fix(subscribestar): match gallery-dl's generic post delimiter (live-feed drift)' (#119) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 38s
CI / integration (push) Successful in 3m15s
2026-06-17 15:07:59 -04:00
bvandeusen 26dcfaf6c2 Merge pull request 'fix(subscribestar): initial feed GET is a navigation, not XHR (first-run drift)' (#118) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 37s
CI / integration (push) Successful in 3m14s
2026-06-17 14:30:02 -04:00
bvandeusen 9b1b0369cc Merge pull request 'SubscribeStar → native core ingester + native-ingest DRY pass (milestone #71)' (#117) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 29s
CI / integration (push) Successful in 3m20s
2026-06-17 12:48:29 -04:00
bvandeusen 18123fb9cb Merge pull request 'fix(external): recovery-sweep threshold + queue recording + split fetch timeouts (#883)' (#116) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m14s
2026-06-16 21:24:31 -04:00
bvandeusen 2e806f202f Merge pull request 'test(artist-dir): fix flaky uq_image_record_sha256 collision' (#115) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 31s
CI / integration (push) Successful in 3m17s
2026-06-16 20:34:21 -04:00
bvandeusen 18d5c05639 Merge pull request 'fix(ml): per-task async engine for recompute_centroid (#881)' (#114) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Failing after 3m19s
2026-06-16 20:24:37 -04:00
bvandeusen 11ddfc3876 Merge pull request 'fix(maint): resurface dedup/gated-purge results after navigate-away (#877)' (#113) from dev into main
Build images / sign-extension (push) Successful in 2s
Build images / build-ml (push) Successful in 6s
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 18s
Build images / build-web (push) Successful in 15s
CI / backend-lint-and-test (push) Successful in 29s
CI / integration (push) Successful in 3m20s
2026-06-16 16:48:52 -04:00
bvandeusen 2b8ce86622 Merge pull request 'Gated Patreon posts: skip on ingest + cleanup tool (#874)' (#112) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 34s
CI / integration (push) Successful in 3m27s
2026-06-16 15:25:34 -04:00
bvandeusen 49bee77cdc Merge pull request 'Video tag quality: cadence sampling + min-frame aggregation + ML thread cap (#747)' (#111) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m15s
2026-06-16 14:08:40 -04:00
bvandeusen c209e3b37e Merge pull request 'Tier-1 video dedup: import-time + retroactive cleanup (#871)' (#110) from dev into main
CI / integration (push) Successful in 3m16s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-web (push) Successful in 26s
2026-06-16 08:55:38 -04:00
bvandeusen cffdd93418 Merge pull request 'Nested-archive extraction (#718) + post-first ingest (#67) + post-body canary (#862)' (#109) from dev into main
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m19s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
2026-06-15 21:37:39 -04:00
bvandeusen fd84be40dd Merge pull request 'External-attach orphan fix (#859) + image-less post display' (#108) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 40s
Build images / build-web (push) Successful in 10s
CI / integration (push) Successful in 3m19s
2026-06-15 01:55:36 -04:00
bvandeusen 79f510d7f8 Merge pull request 'Merge dev → main: read post body from content_json_string (the empty-body fix) (#842)' (#107) from dev into main
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 10s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 37s
CI / integration (push) Successful in 3m24s
Build images / sign-extension (push) Successful in 3s
2026-06-15 00:19:45 -04:00
bvandeusen 59181069da Merge pull request 'Merge dev → main: per-post stdout diagnostics + post-field write DRY (#842/#753)' (#106) from dev into main
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 41s
Build images / sign-extension (push) Successful in 3s
Build images / build-web (push) Successful in 2m15s
Build images / build-ml (push) Successful in 2m55s
CI / integration (push) Successful in 3m11s
2026-06-14 23:45:39 -04:00
bvandeusen 428ecd8642 Merge pull request 'Merge dev → main: per-post body-capture diagnostics in the event UI (#842)' (#105) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / integration (push) Successful in 3m11s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 37s
2026-06-14 23:14:14 -04:00
bvandeusen ed1e04b831 Merge pull request 'Merge dev → main: recapture body-fetch fix + diagnostics (#842)' (#104) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m15s
2026-06-14 22:31:41 -04:00
bvandeusen f5156bd847 Merge pull request 'Merge dev → main: Recapture mode (#842) — re-grab post bodies/links + localize on-disk inline images' (#103) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 32s
Build images / build-web (push) Successful in 2m30s
CI / integration (push) Successful in 3m29s
Build images / build-ml (push) Successful in 3m27s
2026-06-14 21:21:49 -04:00
bvandeusen dfc3922d24 Merge pull request 'Merge dev → main: #830 rich post capture + external-host downloads (+ #768/#789/#739)' (#102) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 48s
Build images / build-web (push) Successful in 2m21s
Build images / build-ml (push) Successful in 2m47s
CI / integration (push) Successful in 3m20s
2026-06-14 19:16:09 -04:00
bvandeusen 3eb08e926b Merge pull request 'fix(aliases): modal raw-key bug + alias visibility/management' (#101) from dev into main
CI / lint (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m13s
2026-06-12 14:02:00 -04:00
bvandeusen 9e81ced359 Merge pull request 'fix(images): percent-encode original-image URLs ('#' in paths 404'd)' (#100) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m15s
2026-06-12 00:44:56 -04:00
bvandeusen 11e9f5af60 Merge pull request 'fix(browse): tabs and search on one row' (#99) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 7s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 29s
Build images / build-web (push) Successful in 11s
CI / integration (push) Successful in 3m13s
2026-06-12 00:28:38 -04:00
bvandeusen 909fa37b15 Merge pull request 'Browse search + series numbering rework + kebab fix' (#98) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-web (push) Successful in 2m17s
Build images / build-ml (push) Successful in 2m50s
CI / integration (push) Successful in 3m16s
2026-06-12 00:14:31 -04:00
bvandeusen dfab8f65ff Merge pull request 'feat(series): flat sequence + cosmetic dividers + pending staging (#789)' (#97) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
CI / frontend-build (push) Successful in 29s
CI / backend-lint-and-test (push) Successful in 38s
Build images / build-web (push) Successful in 2m13s
Build images / build-ml (push) Successful in 2m49s
CI / integration (push) Successful in 3m9s
2026-06-11 22:07:57 -04:00
bvandeusen 618f7cdc36 Merge pull request 'feat(ml): drop image_record.tagger_predictions — image_prediction is sole store (#768 step 3)' (#96) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 25s
Build images / build-web (push) Successful in 2m29s
Build images / build-ml (push) Successful in 3m0s
CI / integration (push) Successful in 3m13s
2026-06-11 19:32:05 -04:00
bvandeusen 028ea33a7c Merge pull request 'fix(migration): make 0045 DDL-only; backfill image_prediction via batched task (#768)' (#95) from dev into main
CI / backend-lint-and-test (push) Successful in 38s
Build images / build-ml (push) Successful in 2m58s
CI / integration (push) Successful in 3m15s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
Build images / build-web (push) Successful in 2m50s
2026-06-11 09:22:22 -04:00
bvandeusen 444c1fb075 Merge pull request 'perf(migration): 0045 streams json_each (no materialize / no temp blowup)' (#94) from dev into main
Build images / sign-extension (push) Successful in 3s
Build images / build-web (push) Successful in 2m14s
Build images / build-ml (push) Successful in 2m36s
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m8s
2026-06-10 22:07:16 -04:00
bvandeusen 26c68b0a75 Merge pull request 'fix(migration): 0045 guards json_each against scalar tagger_predictions' (#93) from dev into main
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m13s
Build images / sign-extension (push) Successful in 2s
Build images / build-web (push) Successful in 6s
Build images / build-ml (push) Successful in 2m40s
2026-06-10 20:33:27 -04:00
bvandeusen e75427b19a Merge pull request '#768 steps 1+2: normalized image_prediction table (read cutover)' (#92) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-web (push) Successful in 2m4s
Build images / build-ml (push) Successful in 2m42s
CI / integration (push) Successful in 3m8s
2026-06-10 20:15:26 -04:00
bvandeusen 5447fab987 Merge pull request 'Activity search + RecoverySweep fix + tagger_predictions shrink (#762, #764) + backup polish' (#91) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 38s
Build images / build-web (push) Successful in 2m23s
Build images / build-ml (push) Successful in 2m51s
CI / integration (push) Successful in 3m10s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
2026-06-10 14:33:35 -04:00
bvandeusen bad37e07b2 Merge pull request 'Browse hub, series rename, full-prediction dropdown + a DRY pass (7 sweeps)' (#90) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 27s
CI / frontend-build (push) Successful in 44s
Build images / build-ml (push) Successful in 3m2s
CI / integration (push) Successful in 3m15s
Build images / build-web (push) Successful in 2m17s
2026-06-10 00:24:01 -04:00
bvandeusen 2bfc9936a1 Merge pull request 'UI batch: tagging flow, series browse, fandom chips, nav' (#89) from dev into main
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 39s
Build images / sign-extension (push) Successful in 3s
CI / frontend-build (push) Successful in 26s
Build images / build-web (push) Successful in 2m21s
Build images / build-ml (push) Successful in 2m52s
CI / integration (push) Successful in 3m24s
2026-06-09 20:48:19 -04:00
bvandeusen 4c6406ee18 Merge pull request 'fix(posts): link duplicate items to every post + prune bare shells' (#88) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-web (push) Successful in 2m19s
Build images / build-ml (push) Successful in 2m41s
CI / integration (push) Successful in 3m5s
2026-06-08 19:42:31 -04:00
bvandeusen bb47e80b3e Merge pull request 'fix(cleanup): unused-tags delete must use the same predicate as the preview' (#87) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m13s
2026-06-08 18:17:49 -04:00
bvandeusen dc1083b5e0 Merge pull request 'Unused-tag fandom fix + ML-worker logging/tuning + unified dropdown Enter' (#86) from dev into main
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m16s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 2m21s
2026-06-08 17:27:55 -04:00
bvandeusen e46893fefd Merge pull request 'Migration lock safety + remove the merge's full-table scan (the real 0040-hang fix)' (#85) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 25s
Build images / build-web (push) Successful in 2m3s
Build images / build-ml (push) Successful in 2m33s
CI / integration (push) Successful in 3m7s
2026-06-08 01:03:06 -04:00
bvandeusen 0666e15211 Merge pull request 'Series manage redesign (FC-6.4) + migration/normalize hardening + UX fixes' (#84) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 25s
Build images / build-web (push) Successful in 1m58s
Build images / build-ml (push) Successful in 2m28s
CI / integration (push) Successful in 3m6s
2026-06-07 21:17:57 -04:00
bvandeusen 747390631d Merge pull request 'FC-6 series authoring + backup/NFS hardening + UX fixes' (#83) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
Build images / build-web (push) Successful in 2m5s
Build images / build-ml (push) Successful in 2m43s
CI / integration (push) Successful in 3m6s
2026-06-07 19:31:04 -04:00
bvandeusen e0d2a20588 Merge pull request 'Modal focus/keyboard polish, Camie-in-autocomplete, re-extract self-resume' (#82) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 28s
Build images / build-web (push) Successful in 2m2s
Build images / build-ml (push) Successful in 2m32s
CI / integration (push) Successful in 3m9s
2026-06-07 12:17:43 -04:00
bvandeusen 1d84f67418 Merge pull request 'Modal: large centered spinner + kebab z-index fix' (#81) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 27s
CI / backend-lint-and-test (push) Successful in 29s
Build images / build-web (push) Successful in 2m3s
Build images / build-ml (push) Successful in 2m35s
CI / integration (push) Successful in 3m7s
2026-06-07 10:57:00 -04:00
bvandeusen 91265df3d6 Merge pull request 'Maintenance-queue health + modal/tagging keyboard pass' (#80) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 25s
Build images / build-web (push) Successful in 2m5s
Build images / build-ml (push) Successful in 2m26s
CI / integration (push) Successful in 3m1s
2026-06-07 10:31:01 -04:00
bvandeusen 11acdb0322 Merge pull request 'Patreon: a missing media file_name is a fallback, not API drift' (#79) from dev into main
CI / lint (push) Successful in 2s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 20s
Build images / sign-extension (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m2s
2026-06-06 23:14:25 -04:00
bvandeusen 2eb9fd5dd0 Merge pull request 'Patreon: enforce the backfill time-box mid-post (stop soft-limit overruns)' (#78) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m3s
2026-06-06 23:00:29 -04:00
bvandeusen 01e5ce1410 Merge pull request 'Patreon: resolve creator campaign from a single-post URL' (#77) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m4s
2026-06-06 21:38:33 -04:00
bvandeusen 3bb94674cf Merge pull request 'Patreon download concurrency cap + immediate backfill kickoff on create' (#76) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m1s
2026-06-06 21:27:22 -04:00
bvandeusen a75c602175 Merge pull request 'Subscriptions UX overhaul + Patreon /cw/ vanity fix' (#75) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m2s
2026-06-06 21:12:02 -04:00
bvandeusen ef8f4f7193 Merge pull request 'Tag-casing acronym fix, Patreon resolver hardening, archive diagnostics, post-card strip' (#74) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m1s
2026-06-06 19:59:32 -04:00
bvandeusen ec3d27b219 Merge pull request 'Tag-maintenance sweep + bug-fix batch: #699 #700 #701 #709 #711 #712 #713 #714' (#73) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m2s
2026-06-06 16:49:00 -04:00
bvandeusen 03bd3b2eda Merge pull request 'Native Patreon ingester + download-engine ownership (plans #697, #703–#708)' (#72) from dev into main
Build images / build-ml (push) Successful in 7s
Build images / build-web (push) Successful in 10s
CI / backend-lint-and-test (push) Successful in 25s
CI / frontend-build (push) Successful in 32s
CI / integration (push) Successful in 3m1s
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
2026-06-06 12:57:31 -04:00
bvandeusen 7395e77d75 Merge pull request 'Smarter backfill: time-boxed chunks, run-until-done (plan #693)' (#71) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
CI / backend-lint-and-test (push) Successful in 13s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 18s
CI / integration (push) Successful in 2m58s
2026-06-05 16:33:06 -04:00
bvandeusen 575d817919 Merge pull request '#70 dev→main: cursor-paged backfill + mobile modal fixes' from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
CI / backend-lint-and-test (push) Successful in 13s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 18s
CI / integration (push) Successful in 2m59s
2026-06-05 11:10:02 -04:00
bvandeusen 2a8f7cd8b6 Merge pull request '#69 dev→main: release v26.06.04.0' from dev into main
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 12s
CI / frontend-build (push) Successful in 19s
CI / integration (push) Successful in 3m2s
Build images / sign-extension (push) Has been skipped
Build images / build-ml (push) Successful in 6s
Build images / build-web (push) Successful in 6s
2026-06-04 23:16:12 -04:00
bvandeusen 83f8af8090 Merge pull request 'dev→main: surface near-duplicate (pHash) control + reorder import tab' (#68) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 6s
CI / backend-lint-and-test (push) Successful in 11s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 20s
CI / integration (push) Successful in 2m56s
2026-06-04 21:39:44 -04:00
bvandeusen 9a2617c1a2 Merge pull request 'dev→main: post-card redesign (images→modal, in-place text expand)' (#67) from dev into main
CI / lint (push) Failing after 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
CI / backend-lint-and-test (push) Successful in 11s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 24s
CI / integration (push) Successful in 2m57s
2026-06-04 17:32:50 -04:00
bvandeusen 81688815a0 Merge pull request 'dev→main: similar-search render fix + reset-content-tagging + scan persistence' (#66) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 9s
CI / backend-lint-and-test (push) Successful in 13s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 22s
CI / integration (push) Successful in 2m58s
2026-06-04 16:59:52 -04:00
bvandeusen 773128c3bf Merge pull request 'dev→main: purpose-built mobile layout for subscriptions hub' (#65) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 6s
CI / backend-lint-and-test (push) Successful in 14s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 38s
CI / integration (push) Successful in 2m56s
2026-06-04 13:43:56 -04:00
bvandeusen ce7b154ae9 Merge pull request 'dev→main: subscriptions table mobile card layout' (#64) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
Build images / build-ml (push) Successful in 8s
CI / backend-lint-and-test (push) Successful in 14s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 22s
CI / integration (push) Successful in 2m57s
2026-06-04 12:56:58 -04:00
bvandeusen 9430a9d9c3 Merge pull request 'dev→main: gallery similarity search (Phase 3) + UI/mobile polish' (#63) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
CI / backend-lint-and-test (push) Successful in 12s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 21s
CI / integration (push) Successful in 2m56s
2026-06-04 11:21:26 -04:00
bvandeusen 23aee56ce3 Merge pull request 'dev→main: showcase cascade + filter styling + DB maintenance + gallery filter Phase 2 + showcase decode-gate + CI perf' (#62) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 9s
CI / backend-lint-and-test (push) Successful in 12s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / integration (push) Successful in 3m0s
2026-06-04 08:27:47 -04:00
bvandeusen 711abea567 Merge pull request 'Gallery speed + fandom editing + filters + pinned filter bar' (#61) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 8s
CI / backend-lint-and-test (push) Successful in 18s
CI / frontend-build (push) Successful in 19s
Build images / build-web (push) Successful in 11s
CI / intimp (push) Successful in 3m48s
CI / intapi (push) Successful in 7m45s
CI / intcore (push) Successful in 8m29s
2026-06-04 00:07:19 -04:00
bvandeusen 844bb86802 Merge pull request 'fix(download): release DB connections across the gallery-dl subprocess' (#60) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 10s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 25s
CI / intimp (push) Successful in 3m46s
CI / intapi (push) Successful in 7m41s
CI / intcore (push) Successful in 8m24s
2026-06-03 22:11:36 -04:00
bvandeusen a8f6a464aa Merge pull request 'fix(download): salvage soft-time-limit kills + fix timeout ladder' (#59) from dev into main
CI / lint (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 21s
CI / frontend-build (push) Successful in 24s
Build images / build-web (push) Successful in 2m37s
Build images / build-ml (push) Successful in 3m21s
CI / intimp (push) Successful in 3m40s
CI / intapi (push) Successful in 7m47s
CI / intcore (push) Successful in 8m15s
2026-06-03 19:35:45 -04:00
bvandeusen ab9922ad2e Merge pull request 'feat(artist): "new since last visit" badge + banner' (#58) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
CI / backend-lint-and-test (push) Successful in 23s
Build images / build-ml (push) Successful in 2m53s
CI / frontend-build (push) Successful in 21s
Build images / build-web (push) Successful in 2m9s
CI / intimp (push) Successful in 3m36s
CI / intapi (push) Successful in 7m32s
CI / intcore (push) Successful in 8m9s
2026-06-03 16:20:54 -04:00
bvandeusen 0533807669 Merge pull request 'feat(ext): verify cookies in-browser before uploading (1.0.7)' (#57) from dev into main
CI / backend-lint-and-test (push) Successful in 32s
Build images / build-ml (push) Successful in 3m28s
Build images / build-web (push) Successful in 3m42s
CI / intapi (push) Successful in 9m32s
CI / frontend-build (push) Successful in 20s
extension / lint (push) Successful in 14s
Build images / sign-extension (push) Successful in 3m17s
CI / intimp (push) Successful in 4m26s
CI / lint (push) Successful in 3s
CI / intcore (push) Successful in 10m18s
2026-06-03 14:17:42 -04:00
bvandeusen 279dff3fb6 Merge pull request 'feat(ml): normalize Camie suggestion names to human-readable' (#56) from dev into main
Build images / build-ml (push) Successful in 2m55s
CI / intimp (push) Successful in 3m36s
CI / intapi (push) Successful in 7m38s
CI / intcore (push) Successful in 8m24s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 15s
CI / frontend-build (push) Successful in 17s
Build images / build-web (push) Successful in 2m17s
2026-06-03 13:18:44 -04:00
bvandeusen 37e66cddc4 Merge pull request 'chore(modal): drop ?image=N soft-compat — pure overlay' (#55) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
CI / frontend-build (push) Successful in 27s
CI / backend-lint-and-test (push) Successful in 38s
Build images / build-ml (push) Successful in 2m46s
Build images / build-web (push) Successful in 2m36s
CI / intimp (push) Successful in 3m42s
CI / intapi (push) Successful in 7m25s
CI / intcore (push) Successful in 8m12s
2026-06-02 19:35:04 -04:00
bvandeusen 9cf6b2d363 Merge pull request 'audit-g5 final + ML threshold default + kebab menu fix' (#54) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 4s
CI / backend-lint-and-test (push) Successful in 23s
CI / frontend-build (push) Successful in 27s
Build images / build-web (push) Successful in 3m0s
Build images / build-ml (push) Successful in 3m45s
CI / intimp (push) Successful in 3m46s
CI / intapi (push) Successful in 8m8s
CI / intcore (push) Successful in 8m34s
2026-06-02 19:09:49 -04:00
bvandeusen 6ef0fed41f Merge pull request 'audit-g5: architectural debt — 4 bundles (A/B/C/D)' (#53) from dev into main
CI / lint (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 9s
CI / backend-lint-and-test (push) Successful in 18s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 26s
CI / intimp (push) Successful in 3m51s
CI / intapi (push) Successful in 7m38s
CI / intcore (push) Successful in 8m52s
2026-06-02 18:07:25 -04:00
bvandeusen 89b48f8f35 Merge pull request 'audit-g4: status-enum miss batch' (#52) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 11s
CI / backend-lint-and-test (push) Successful in 13s
Build images / build-web (push) Successful in 10s
CI / frontend-build (push) Successful in 35s
CI / intimp (push) Successful in 3m41s
CI / intapi (push) Successful in 7m25s
CI / intcore (push) Successful in 8m10s
2026-06-02 16:15:00 -04:00
bvandeusen d60e0b9494 Merge pull request 'audit-g3: lifecycle batch — recovery sweeps, retention, timeouts' (#51) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 9s
CI / backend-lint-and-test (push) Successful in 12s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 36s
CI / intimp (push) Successful in 3m39s
CI / intapi (push) Successful in 7m39s
CI / intcore (push) Successful in 8m10s
2026-06-02 14:49:28 -04:00
bvandeusen 9c27a2d3c7 Merge pull request 'audit-g2: async race / state-leak fixes across eight stores' (#50) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
CI / backend-lint-and-test (push) Successful in 23s
CI / frontend-build (push) Successful in 37s
Build images / build-web (push) Successful in 13s
CI / intimp (push) Successful in 3m52s
CI / intapi (push) Successful in 8m15s
CI / intcore (push) Successful in 8m50s
2026-06-02 14:17:12 -04:00
bvandeusen 93e37681b7 Merge pull request 'audit-g1: six one-liner drift fixes from 2026-06-02 audit' (#49) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 11s
CI / backend-lint-and-test (push) Successful in 11s
CI / frontend-build (push) Successful in 19s
Build images / build-web (push) Successful in 10s
CI / intapi (push) Successful in 7m28s
CI / intcore (push) Successful in 8m3s
CI / intimp (push) Successful in 3m42s
2026-06-02 13:29:17 -04:00
bvandeusen 64ca858574 Merge pull request 'UX fixes: suggestion-accept chip refresh, showcase endless feed, non-media downloads' (#48) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Successful in 9s
CI / backend-lint-and-test (push) Successful in 12s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 20s
CI / intimp (push) Successful in 3m52s
CI / intapi (push) Successful in 7m45s
CI / intcore (push) Successful in 8m20s
2026-06-02 08:26:58 -04:00
bvandeusen 9d0c0b7da8 Merge pull request 'fix(thumbnails): surface backfill counts + tighten validity check' (#47) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 9s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 18s
CI / intimp (push) Successful in 3m45s
CI / intapi (push) Successful in 7m32s
CI / intcore (push) Successful in 8m4s
2026-06-01 22:34:38 -04:00
bvandeusen 8e4d252ae4 Merge pull request 'fix(downloads): enqueue thumbnail + ML per attached image' (#46) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / intimp (push) Successful in 3m40s
CI / lint (push) Successful in 3s
CI / intapi (push) Successful in 7m51s
CI / intcore (push) Successful in 8m46s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / backend-lint-and-test (push) Successful in 19s
CI / frontend-build (push) Successful in 26s
2026-06-01 21:52:42 -04:00
bvandeusen fdd3e01f56 Merge pull request 'ux(failing-sources): visible row separators + clearer hover' (#45) from dev into main
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-ml (push) Successful in 10s
Build images / build-web (push) Successful in 10s
CI / backend-lint-and-test (push) Successful in 20s
CI / frontend-build (push) Successful in 33s
CI / intimp (push) Successful in 3m44s
CI / intapi (push) Successful in 7m48s
CI / intcore (push) Successful in 8m16s
2026-06-01 21:09:28 -04:00
bvandeusen c82fb308b6 Merge pull request 'Post.source_id refactor + tick/backfill modes + PARTIAL classifier + Mux fix + UX' (#44) from dev into main
Build images / build-ml (push) Failing after 2s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
CI / intimp (push) Successful in 3m47s
CI / intapi (push) Successful in 7m52s
CI / intcore (push) Successful in 8m16s
Build images / build-web (push) Successful in 12s
CI / backend-lint-and-test (push) Successful in 19s
CI / frontend-build (push) Successful in 31s
2026-06-01 20:44:23 -04:00
bvandeusen 8cf8d2ca4d Merge pull request 'Modal Esc/overflow polish, artist-scoped post scroll, failing-sources Logs button' (#43) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 8s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 2m45s
Build images / build-ml (push) Successful in 3m31s
CI / intimp (push) Successful in 3m48s
CI / intapi (push) Successful in 7m11s
CI / intcore (push) Successful in 7m47s
2026-06-01 12:10:11 -04:00
bvandeusen b1d58bc3b8 Merge pull request 'fix(ci): POSIX-safe SHORT_SHA in build.yml' (#42) from dev into main
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 15s
CI / frontend-build (push) Successful in 20s
Build images / sign-extension (push) Successful in 3s
Build images / build-web (push) Successful in 2m17s
Build images / build-ml (push) Successful in 3m24s
CI / intimp (push) Successful in 3m22s
CI / intapi (push) Successful in 7m51s
CI / intcore (push) Successful in 8m5s
2026-06-01 08:03:17 -04:00
bvandeusen 65386f02a0 Merge pull request 'View modal batch: autofocus, suggestions UX, post-title click, retire copyright/artist' (#41) from dev into main
Build images / sign-extension (push) Successful in 2s
CI / lint (push) Successful in 2s
Build images / build-ml (push) Failing after 4s
Build images / build-web (push) Failing after 3s
CI / backend-lint-and-test (push) Successful in 15s
CI / frontend-build (push) Successful in 17s
CI / intimp (push) Successful in 3m46s
CI / intapi (push) Successful in 7m17s
CI / intcore (push) Successful in 8m2s
2026-06-01 07:01:42 -04:00
bvandeusen 667b05f14e Merge pull request 'Extension probe-and-add (v1.0.6) + per-commit image tags' (#40) from dev into main
CI / lint (push) Successful in 2s
Build images / build-ml (push) Failing after 2s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 22s
extension / lint (push) Successful in 8s
Build images / sign-extension (push) Successful in 1m51s
Build images / build-web (push) Failing after 4s
CI / intimp (push) Successful in 3m38s
CI / intapi (push) Successful in 7m20s
CI / intcore (push) Successful in 7m45s
2026-06-01 01:44:03 -04:00
bvandeusen 856e9104b4 Merge pull request 'Sidecar synthetic anchor cleanup + tier-gated classifier fix' (#39) from dev into main
CI / lint (push) Successful in 5s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 28s
CI / intimp (push) Successful in 3m54s
Build images / sign-extension (push) Has been skipped
Build images / build-web (push) Successful in 1m1s
Build images / build-ml (push) Successful in 1m21s
CI / intapi (push) Successful in 7m31s
CI / intcore (push) Successful in 8m0s
2026-06-01 00:16:58 -04:00
bvandeusen 0397642b21 Merge pull request 'Showcase cadence tuning + cooldown-aware bulk retry' (#38) from dev into main 2026-05-30 23:50:36 -04:00
bvandeusen 237575447d Merge pull request 'Thumbnail URL fix + archive daemon fix + batched initial loads' (#37) from dev into main 2026-05-30 22:01:43 -04:00
bvandeusen ed358757dc Merge pull request 'Most-overdue-first scheduling + rich timeout diagnostics' (#36) from dev into main 2026-05-30 14:30:41 -04:00
bvandeusen d181f4afb8 Merge pull request 'Downloads burst-prevention + maintenance-menu fix + gdl timeout' (#35) from dev into main 2026-05-30 11:43:18 -04:00
bvandeusen 2886fa4997 Merge pull request 'Tooltip !important fix — 104cac5 follow-up after Vite CSS reorder' (#34) from dev into main 2026-05-30 00:02:24 -04:00
bvandeusen f256f587ee Merge pull request 'UI batch + I1–I6 service passes + download-event recovery sweep' (#33) from dev into main 2026-05-29 22:46:16 -04:00
bvandeusen 384d8d5e50 Merge pull request 'Dashboard insights + project-wide DRY pass' (#32) from dev into main 2026-05-28 15:38:26 -04:00
bvandeusen 319e8c1d18 Merge pull request 'v26.05.28.0: downloads dashboard + task-resilience overhaul (timeouts, archive split, 3-layer poison-pill defense)' (#31) from dev into main 2026-05-28 00:45:00 -04:00
bvandeusen 9075d8eadd Merge pull request 'v26.05.27.2: subscribestar + HF cookie quirks, platforms package refactor, showcase IR-parity, secure-context audit' (#30) from dev into main 2026-05-27 21:34:02 -04:00
bvandeusen 88e53e5b86 Merge pull request 'v26.05.27.1: subscriptions hub + post-card merge + sidecar audit' (#29) from dev into main 2026-05-27 17:12:48 -04:00
bvandeusen 37e8b796a1 Merge pull request 'v26.05.27.0: PostCard redesign + IR-style tag suffix + drop meta/rating + extension v1.0.4 CSP fix' (#28) from dev into main 2026-05-27 11:31:18 -04:00
bvandeusen 4e82208926 Merge pull request 'v26.05.26.5 — extension CORS unblock + UI gap closes + CI workflow cleanup' (#27) from dev into main 2026-05-26 20:15:07 -04:00
bvandeusen 52fff00353 Merge pull request 'v26.05.26.4 — hotfix: migration 0022 pre-DELETE colliding ImageProvenance before UPDATE' (#26) from dev into main 2026-05-26 18:06:20 -04:00
bvandeusen c14338cbce Merge pull request 'v26.05.26.3 — hotfix: migration 0022 pre-merge across ENTIRE (canonical+others) group' (#25) from dev into main 2026-05-26 17:52:59 -04:00
bvandeusen 8c36dd28b0 Merge pull request 'v26.05.26.2 — hotfix: alembic 0022 Post-collision pre-merge + ci.yml cache continue-on-error' (#24) from dev into main 2026-05-26 16:50:43 -04:00
bvandeusen 88cfb3dd02 Merge pull request 'v26.05.26.1 — thumb backfill, modal redesign, recovery sweep race-safety, artist view redesign, extension fixes' (#23) from dev into main 2026-05-26 16:32:00 -04:00
bvandeusen 5d4f223b71 Merge pull request 'Release v26.05.25.7 — FC-Cleanup tab + UniqueViolation fix + error modal + extension install fix' (#22) from dev into main 2026-05-26 08:26:46 -04:00
bvandeusen 05090c6e85 Merge pull request 'Release v26.05.25.7 — animated-WebP worker fix + FC-Cleanup backend' (#21) from dev into main 2026-05-26 01:48:13 -04:00
bvandeusen 3a577d5ade Merge pull request 'fix(ext-ci): use browser_download_url + curl -f + ZIP magic check (XPI silently corrupt)' (#20) from dev into main 2026-05-26 00:43:02 -04:00
bvandeusen f4fe02e346 Merge pull request 'fix(ext-ci): drop actions/upload-artifact (Forgejo doesn't support v4+ GHES)' (#19) from dev into main 2026-05-25 23:33:40 -04:00
bvandeusen e766197d99 Merge pull request 'fix(ext-ci): jq→python + bump ext to 1.0.3 + rollback-on-upload-failure' (#18) from dev into main 2026-05-25 23:14:51 -04:00
bvandeusen 3872e1dda9 Merge pull request 'fix(ext-ci): web-ext v8 .cjs config workaround' (#17) from dev into main 2026-05-25 22:49:14 -04:00
bvandeusen 9814f3dbaf Merge pull request 'Release v26.05.25.5 — Extension publish refactor, deep-scan IR-parity, archive-import perf, artist Settings tab' (#16) from dev into main 2026-05-25 22:44:59 -04:00
bvandeusen b214460fdb Merge pull request 'Release v26.05.25.4 — importer ext sanitize fix, CI shard split, BrowserExtensionCard on Overview' (#15) from dev into main 2026-05-25 21:11:50 -04:00
bvandeusen ac55d0e8d8 Merge pull request 'fix(ext-ci): match AMO-renamed signed XPI' (#14) from dev into main 2026-05-25 18:22:50 -04:00
bvandeusen 89a89e0ded Merge pull request 'Release v26.05.25.3 — ML embedder SigLIP fix, import-UX, extension publish' (#13) from dev into main 2026-05-25 17:56:50 -04:00
bvandeusen 4e9aac2c05 Merge pull request 'v26.05.25.2: supersede + sidecar enrichment, scan toast feedback, CI uv + pip cache + durations' (#12) from dev into main 2026-05-25 14:30:25 -04:00
bvandeusen 2879ac6f2b Merge pull request 'v26.05.25.1: maintenance sweep + Camie v2 + corrupt-file handling + post-date gallery + clear-stuck escape hatch' (#11) from dev into main 2026-05-25 12:57:46 -04:00
bvandeusen b8dce6c483 Merge pull request 'FC-3h + FC-3k: backup first-class + admin destructive actions' (#10) from dev into main 2026-05-25 01:41:53 -04:00
bvandeusen d1c0b82a22 Merge pull request 'v26.05.24.3: FC-3i System Activity dashboard + migration backup-gate retired + modal Escape' (#9) from dev into main 2026-05-24 21:47:53 -04:00
bvandeusen 5526b8dc78 Merge pull request 'v26.05.24.2: IR Post/Provenance restore + modal artist fallback' (#8) from dev into main 2026-05-24 14:30:06 -04:00
bvandeusen 16eb7075c4 Merge pull request 'v26.05.24.1: FC-3g Firefox extension + worker resilience + UI/migration fixes' (#7) from dev into main 2026-05-24 12:52:31 -04:00
bvandeusen 885dcf64f3 Merge pull request 'v26.05.24.0: TopNav re-fix (flex 1 1 0 side cells)' (#6) from dev into main 2026-05-23 22:49:29 -04:00
bvandeusen f2f6b6d25e Merge pull request 'v26.05.23.3: dogfood UX polish + accurate active-batch stats' (#5) from dev into main 2026-05-23 22:05:59 -04:00
bvandeusen 0822240fde Merge pull request 'v26.05.23.2: serve /images + artist cleanup migrator' (#4) from dev into main 2026-05-23 12:19:16 -04:00
bvandeusen 27f7f3fd01 Merge pull request 'v26.05.23.1: migration durability + dogfood UX' (#3) from dev into main 2026-05-23 11:21:33 -04:00
bvandeusen c5bf564f53 Merge dev: v26.05.23.0 migration follow-ups (#2)
pg_dump + zstd in runtime image, lift Quart body cap to 1 GiB. See PR #2.
2026-05-22 22:37:06 -04:00
bvandeusen 602c7d275d Merge dev: FC-1 → FC-5 v1 build (#1)
First merge of `dev` into `main` for FabledCurator. Brings FC-1 (Foundation) through FC-5 (Migration tooling) onto `main`. See PR #1 body for the full stage rollup.
2026-05-22 14:15:45 -04:00
21 changed files with 718 additions and 1917 deletions
File diff suppressed because it is too large Load Diff
+22 -39
View File
@@ -2,7 +2,7 @@ name: CI
# CI lanes per FabledRulebook/forgejo.md "CI philosophy":
# - lint: ruff only, no dep install — fast-fail for the common lint bounce.
# - extension-version: the derived version resolves and is a shape AMO takes.
# - extension-version: the derived version resolves and MAJOR.MINOR agrees.
# - backend-lint-and-test: `pytest -m "not integration"`, no service containers.
# - frontend-build: vitest unit + vite build.
# - integration: pgvector + redis service containers; alembic + `pytest -m integration`.
@@ -35,10 +35,7 @@ jobs:
- name: Ruff lint
# agent/ included so the GPU-agent is linted before its image is built
# (build.yml only `docker build`s it — this is where it gets checked).
# scripts/ likewise: release_notes.py runs only on a tag push, so a
# syntax or import error there would otherwise surface at the one
# moment nobody wants to debug a workflow.
run: ruff check backend/ tests/ alembic/ agent/ scripts/
run: ruff check backend/ tests/ alembic/ agent/
- name: Agent syntax check
# The agent's runtime deps (torch/transformers/ultralytics) aren't in the
# CI image, so we can't import it — but compileall parses every module,
@@ -58,12 +55,9 @@ jobs:
# the extension.yml suite runs on node:24-slim, which is exactly why
# version.spec.js sticks to packaging.sh's git-free subcommands.
# 1. the derivation actually resolves on this commit
# 2. the derived string is one AMO will accept, checked against Mozilla's
# own published grammar rather than a loose "digits and dots"
#
# The MAJOR.MINOR-agreement check that used to be (2) is gone with milestone
# 318 step 8: the committed version no longer seeds anything, so there is no
# hand-set part left for the two files to disagree about.
# 2. MAJOR.MINOR agrees between the two files — the one part still hand-set,
# and packaging.sh reads it from manifest.json ALONE, so a divergence
# ships a version package.json disagrees with
#
# Deliberately NOT checked here: that the derived value beats what has already
# been signed. That guard belongs in build.yml, where it compares against the
@@ -88,38 +82,27 @@ jobs:
# busybox sh on the act_runner — no bashisms (family rule).
VERSION=$(sh extension/scripts/packaging.sh version)
echo "derived: $VERSION"
# Mozilla's published grammar for AMO, transcribed verbatim from
# MDN's manifest.json/version page:
#
# ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
#
# Not the looser `^[0-9]+(\.[0-9]+)*$` this lane used to carry. That
# one passes `2026.08.29.0201`, which AMO REJECTS — a segment must be
# the single digit 0 or start 1-9 — and it also passes five segments,
# where AMO allows four. Both would surface as a failed sign with the
# version already burned: AMO 409s on re-signing, so a rejected value
# cannot be reclaimed and cannot be reused. This lane is the cheap
# place to find out. (#3138, milestone 318 step 8.)
if ! echo "$VERSION" | grep -qE '^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$'; then
echo "ERROR: derived version '$VERSION' is not a version AMO accepts."
echo "AMO's grammar: ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$"
echo "Most likely cause: a zero-padded segment (08, 0201). The rest"
echo "of the family pads; the extension must not — see packaging.sh."
# The shape AMO accepts, and the shape build.yml will stamp.
if ! echo "$VERSION" | grep -qE '^[0-9]+(\.[0-9]+)*$'; then
echo "ERROR: derived version '$VERSION' is not plain dotted-numeric."
echo "AMO would reject it, and build.yml stamps it verbatim."
exit 1
fi
# ...and the shape this project actually derives. AMO would happily
# take `1.0.3500147` too, so the grammar check alone would not notice
# a regression to the pre-318 shape — which orders BELOW everything
# signed since, and is unrecoverable once Firefox has the higher one.
if ! echo "$VERSION" | grep -qE '^20[0-9][0-9]\.[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,4}$'; then
echo "ERROR: derived version '$VERSION' is not YYYY.M.D.HHMM."
echo "Rule 148's CalVer is what build.yml signs; the old"
echo "1.0.<minutes> shape would order below every ext-2026.* release."
mm() { grep -E '"version"' "$1" | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([0-9]+\.[0-9]+).*/\1/'; }
MAN=$(mm extension/manifest.json)
PKG=$(mm extension/package.json)
test -n "$MAN" || { echo "ERROR: no parseable version in extension/manifest.json"; exit 1; }
test -n "$PKG" || { echo "ERROR: no parseable version in extension/package.json"; exit 1; }
if [ "$MAN" != "$PKG" ]; then
echo "ERROR: MAJOR.MINOR disagrees between the two files."
echo " extension/manifest.json = $MAN <- packaging.sh reads MAJOR.MINOR from here"
echo " extension/package.json = $PKG"
echo "Only MAJOR.MINOR is hand-set. The patch component is derived from"
echo "commit time and overwritten at build time, so the committed patch"
echo "numbers are inert — but MAJOR.MINOR still ships. Set both the same."
exit 1
fi
echo "OK: derived version $VERSION"
echo "OK: MAJOR.MINOR $MAN, derived version $VERSION"
backend-lint-and-test:
runs-on: python-ci
-80
View File
@@ -1,80 +0,0 @@
name: Release
# A `v*` tag publishes a changelog. It does NOT build anything.
#
# Milestone 318 step 2 removed the tag trigger from build.yml: by the time
# anyone tags a commit, `main` has already built and published it, and a
# rebuild would re-push `:c-<sha>` — which rule 145 forbids even when the
# source matches, since image configs carry timestamps and "same source" does
# not mean "same manifest". That left the tag with no consequence at all.
#
# This is the consequence it has instead. Step 6 put the derived version in the
# Settings footer, so an operator can say WHICH build they are running; this
# says what is IN it that was not in the one they ran last month. Both halves
# of one question (note #3127 §5).
#
# Nothing here runs on a schedule and nothing auto-tags on merge. Release tags
# are bookmarks — cut one when you will want to point at that day by name,
# otherwise don't (note #3127 §0). FC went twelve weeks between v26.06.04.0 and
# the next one and nothing was wrong. A schedule would turn an optional
# bookmark back into ceremony, which is the thing this milestone is removing.
#
# Cutting the tag is an explicit operator action under rule 2 ("`main` — never
# without explicit request", which since 2026-08-28 covers PR, merge and tag
# alike). This lane only decides what happens once they do.
#
# Requires repo secret RELEASE_TOKEN with the `write:release` scope — the same
# PAT build.yml uses for the ext-<version> XPI asset cache.
on:
push:
tags: ['v*']
# So a release body can be regenerated after the fact — the publisher PATCHes
# an existing release rather than falling through on a conflict, so re-running
# this on a tag rewrites the body instead of silently keeping the first one
# (note #3127 §6.7).
workflow_dispatch:
inputs:
tag:
description: 'Tag to (re)publish notes for'
required: true
jobs:
changelog:
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-python:3.14
steps:
- uses: actions/checkout@v4
with:
# Load-bearing twice over: the previous release is found by walking
# ancestry back through the tag graph, and the cross-check against
# the derived web version calls artifacts.sh, which reads commit
# times. A shallow clone would find no previous tag and emit the
# entire history as the changelog — plausible-looking and wrong.
fetch-depth: 0
ref: ${{ github.event.inputs.tag || github.ref }}
# The `:c-<sha>` rollback refs are only real if `main` built this commit.
# The script checks that against origin/main and downgrades the claim to
# "unverified" when it cannot resolve one; fetching it here means that
# downgrade stays an actual signal instead of firing on every release.
- name: Make main's history resolvable
run: git fetch --no-tags --quiet origin +main:refs/remotes/origin/main || true
# TAG goes through the environment, not through `${{ }}` inside the
# run block. The value is operator-supplied, and an expression expanded
# into a shell line is expanded BEFORE the shell sees it — there is no
# quoting that makes that safe. On a tag push it is empty and the script
# falls back to GITHUB_REF.
- name: Publish the derived changelog
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAG: ${{ github.event.inputs.tag }}
run: |
set -eu
if [ -n "${TAG:-}" ]; then
python3 scripts/release_notes.py "$TAG"
else
python3 scripts/release_notes.py
fi
+3 -9
View File
@@ -58,17 +58,11 @@ COPY --from=frontend-builder /build/dist ./frontend/dist
# exactly the shape every reader already has to handle.
#
# Declared LAST on purpose. An ARG/ENV invalidates every layer below it, and
# these are the values that differ between builds of otherwise identical
# source — put them any earlier and the two channels could never share a
# cached pip install.
#
# FC_VERSION is what the instance reports about itself in the UI. Since
# milestone 318 stopped publishing version image tags, that self-report is
# the only answer to "which build is this?" — nothing else names it.
# this is the one value that differs between the dev and main builds of
# identical source — put it any earlier and the two channels could never share
# a cached pip install.
ARG FC_CHANNEL=""
ENV FC_CHANNEL=${FC_CHANNEL}
ARG FC_VERSION=""
ENV FC_VERSION=${FC_VERSION}
EXPOSE 8080
+4 -38
View File
@@ -10,35 +10,6 @@ In production. `main` is continuously deployed — every merge to `main` builds
and publishes `:latest` images, so whatever is on `main` is what is running.
Day-to-day work happens on `dev`, which publishes `:dev` images.
## Versions and tags
Three image tags exist, and no others:
| Tag | Branch | Meaning |
| --- | --- | --- |
| `:latest` | `main` | Production. Moves on every merge. |
| `:c-<sha>` | `main` | Immutable — the rollback unit, all three images together. |
| `:dev` | `dev` | The rolling test channel. Moves on every push. |
There are deliberately **no version tags**. Nothing pins one, and a per-build
name nobody reads is upkeep for a model FC does not run (family rule 145; the
reasoning is note #3127 §5). Rolling back is `docker pull …:c-<sha>`.
Each artifact still has a version, derived rather than chosen: the commit time
of the newest change to that artifact's *own* shipped files, as
`YYYY.MM.DD.HHMM` UTC (rule 148). Four artifacts, four independent versions —
a push touching only `agent/` re-versions the agent and leaves web and ml
alone, and CI skips the builds whose content did not move.
Because no registry name carries it, the running instance's own report is the
only answer to "which build is this?". The foot of Settings shows
`FabledCurator 2026.08.29.0201 · dev`, and `/api/health` returns the same two
fields.
Release tags are optional bookmarks — FC went twelve weeks without one and
nothing was wrong. Pushing `v<version>` publishes a Forgejo release listing the
commits since the previous tag; it builds no image.
## What's in here
Five deployable pieces, built by `.forgejo/workflows/build.yml`:
@@ -81,10 +52,9 @@ FabledCurator is designed to run inside a self-hosted homelab environment over p
## CI / Forgejo setup
Four workflows: `ci.yml` (lint, extension-version check, backend unit tests,
Three workflows: `ci.yml` (lint, extension-version check, backend unit tests,
frontend build, integration), `extension.yml` (extension lint, vitest, XPI
content verification), `build.yml` (sign + publish), and `release.yml`, which
runs only on a `v*` tag and publishes a changelog without building anything.
content verification), and `build.yml` (sign + publish).
**The toolchain each job runs in is its `container.image`, not its `runs-on`
label.** `runs-on: python-ci` only schedules the job onto a runner; every job
@@ -101,12 +71,8 @@ The repo expects one secret:
Generate at https://git.fabledsword.com/user/settings/applications. The injected `GITHUB_TOKEN` cannot be used because it lacks `write:package`.
AMO signing additionally needs `MOZILLA_AMO_JWT_KEY` / `MOZILLA_AMO_JWT_SECRET`.
It runs on **both** channels and is cached per version: because the version is
derived from commit time, `dev` and `main` derive the same number for the same
source, so `main` finds `dev`'s signature already cached and makes no second AMO
call. That cache is why signing must be one-shot — AMO rejects a re-signed
version.
AMO signing additionally needs `MOZILLA_AMO_JWT_KEY` / `MOZILLA_AMO_JWT_SECRET`; it runs on
`main` only and is cached per version, since AMO rejects a re-signed version.
## License
+5 -8
View File
@@ -7,13 +7,13 @@ from __future__ import annotations
import asyncio
import hashlib
import hmac
import os
import re
from pathlib import Path
from quart import Blueprint, jsonify, request
from sqlalchemy import select
from ..build_info import FC_CHANNEL as _FC_CHANNEL
from ..extensions import get_session
from ..models import AppSetting
from ..services.extension_service import (
@@ -33,13 +33,10 @@ XPI_DIR = Path("/app/frontend/dist/extension")
_XPI_VERSION_RE = re.compile(r"fabledcurator-(?P<version>[\w.-]+)\.xpi$")
# Which channel this image belongs to — "dev" or "main" — baked in at build
# time (milestone 271 step 7). Read from build_info rather than the environment
# a second time: /api/health reports the same value, and two independent
# `os.environ.get` calls are two things that can drift.
#
# Still bound as a module-level name here, so tests monkeypatch
# `extension.FC_CHANNEL` exactly as they did before, same as XPI_DIR above.
FC_CHANNEL = _FC_CHANNEL
# time from the FC_CHANNEL build arg (milestone 271 step 7). Empty for a local
# build, or for any image predating the field. Tests override by monkeypatching
# this constant, same as XPI_DIR above.
FC_CHANNEL = os.environ.get("FC_CHANNEL", "").strip()
async def _ext_key_required(session) -> bool:
+2 -17
View File
@@ -1,20 +1,5 @@
"""Health endpoint — no DB or Redis touch; liveness, plus the build's identity.
The identity rides here rather than on a route of its own because it answers
at the same cost: two module constants, no I/O, nothing that can be slow or
fail. It is also already fetched app-wide — TopNav calls `refreshHealth` on
mount — so a separate endpoint would mean a second request for two strings.
Both fields are OMITTED when unset rather than sent empty. See build_info.
"""
from ..build_info import FC_CHANNEL, FC_VERSION
"""Health endpoint — no DB or Redis touch; just liveness."""
async def get_health():
body = {"status": "ok"}
if FC_VERSION:
body["version"] = FC_VERSION
if FC_CHANNEL:
body["channel"] = FC_CHANNEL
return body, 200
return {"status": "ok"}, 200
-30
View File
@@ -1,30 +0,0 @@
"""What this build IS — stamped at image build time, not configurable.
Deliberately separate from `config.py`. Those are operator settings, read from
the environment and meant to be changed. These describe the artifact itself and
are baked in by CI (the `FC_VERSION` / `FC_CHANNEL` build args); an operator
setting them by hand is not a supported thing to do, it is just how a value
gets from the build into the running process.
**Absent rather than empty when unknown.** A locally-built image has no version,
and neither did any image predating the field — one spelling of "cannot say",
which every reader already has to handle, instead of a second one to
special-case (note #3127 §7).
**Why this matters more than it used to.** Milestone 318 stopped publishing
version image tags, so a running instance's self-report is now the *only*
answer to "which build is this?" — there is no registry name left to check it
against. A wrong value here has nothing to contradict it. That is why the UI
renders `unknown` rather than a blank or a plausible default: an empty footer
reads as "no version", which is a different and false claim.
The channel lives BESIDE the version and is never folded into it (rule 149).
A `-dev` suffix would be parsed by the extension's comparator as a segment
worth 0, making every dev build compare equal to every other — issue #2993's
exact failure.
"""
import os
FC_VERSION = os.environ.get("FC_VERSION", "").strip()
FC_CHANNEL = os.environ.get("FC_CHANNEL", "").strip()
+29 -98
View File
@@ -9,17 +9,14 @@ git.fabledsword.com/bvandeusen/ci-python:3.14
## Image deps used
- python 3.14
- ruff (analyzer for `backend/`, `tests/`, `alembic/`, `agent/`, `scripts/`)
- ruff (analyzer for `backend/`, `tests/`, `alembic/`)
- node (frontend job: `npm install` + vitest + vite build)
- docker CLI + buildx (`.forgejo/workflows/build.yml`: build-web, build-ml, build-agent — Fabled-Git registry push, and `imagetools inspect`/`create` for the reuse path)
- docker CLI + buildx (`.forgejo/workflows/build.yml`: build-web, build-ml — Fabled-Git registry push)
## Secondary runtime image
node:24-bookworm-slim — `.forgejo/workflows/extension.yml` only.
`.forgejo/workflows/release.yml` runs on `ci-python:3.14` like everything else
and installs nothing: it needs git and stdlib python, and builds no image.
The extension lane is the one job that does NOT run on `ci-python:3.14`: it
needs a current Node for `web-ext` and vitest and nothing Python at all. Kept
on the upstream slim image rather than adding a Node toolchain to `ci-python`,
@@ -57,100 +54,34 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
shims to production code — the libs ship as `background.scripts`, not ES
modules, so the specs exercise exactly the bytes packaged into the XPI.
- **`extension/scripts/packaging.sh` is the single definition of what ships
inside the XPI.** Three consumers read from it rather than keeping their own
copy: web-ext's `--ignore-files` (`extension/package.json`), the `git log`
pathspec inside the script's own version derivation, and `scripts/artifacts.sh`,
which appends the extension's set to web's because the web image bundles the
signed XPI. Hand-kept copies of that one fact is what allowed issue #2397, so
`extension/test/version.spec.js` asserts no workflow has reintroduced a
literal `:(exclude)extension/…`.
- **Packaged and version-relevant are two different sets** (#3156). `scripts/`
is excluded from the XPI and is NOT excluded from the version derivation,
because `packaging.sh` decides the version string stamped into the packaged
`manifest.json`. The membership test is *"can changing this file change the
published bytes?"*, not *"is this file copied in?"* — which is why the script
keeps two lists rather than one.
inside the XPI.** Two consumers read from it rather than keeping their own
copy: web-ext's `--ignore-files` (`extension/package.json`), and the `git log`
pathspec inside the script's own version derivation. It was three until
2026-08-27 — `ci.yml`'s `extension-version` guard held the third and went when
the manual bump it guarded did (milestone 271 step 5). Hand-kept copies of
that one fact is what allowed issue #2397, so `extension/test/version.spec.js`
asserts no workflow has reintroduced a literal `:(exclude)extension/…`.
- **The shipped extension version is derived, not committed.** It is the commit
TIME of the newest packaged-extension change, rendered `YYYY.M.D.HHMM` UTC
(family rules 148/149 — never a commit count, which orders by branch rather
than by recency). `build.yml`'s `sign-extension` computes it and stamps it
into `extension/manifest.json` + `package.json` in the working tree before
signing; the stamp is never committed. The version in the repo is **wholly
inert** — since milestone 318 step 8 there is no hand-set MAJOR.MINOR either.
- **The extension is the one artifact that does not zero-pad, and that is not a
drift** (#3138). Mozilla's grammar for AMO is
`^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$` — a segment is the single
digit `0` or starts 1-9, and there are at most four. `2026.08.29.0201` is
rejected; `2026.8.29.201` is the same value one character narrower per
segment, and rule 148 defines comparison as numeric per segment, so nothing is
reordered. `ci.yml`'s `extension-version` lane asserts the derived string
against that exact regex, plus a `YYYY.M.D.HHMM` shape check that would catch
a regression to the pre-318 `1.0.<minutes>` — which AMO would accept and which
orders below everything already signed. Checking here is the whole point: AMO
409s on re-signing, so a version it rejects is burned and cannot be reused.
`scripts/artifacts.sh version extension` **delegates** to `packaging.sh` so
the two cannot answer differently.
- Every job that derives anything checks out with `fetch-depth: 0` — all four
`build.yml` jobs, `ci.yml`'s `extension-version` and `backend-lint-and-test`
(for `tests/test_artifact_paths.py` and `test_artifact_identity.py`), and
`release.yml`, which additionally walks the tag graph. A depth-1 clone sees
one commit and derives a wrong, too-low value **rather than failing**, so the
full-history checkout is load-bearing rather than incidental.
- **`scripts/artifacts.sh` is the same shape one level up: one definition per
artifact of what it is built from, and the two values derived from it.**
`revision` (12 hex of the newest commit touching that set) and `version`
(`YYYY.MM.DD.HHMM` UTC, rule 148). Four artifacts, four independent answers,
so a push touching only `agent/` leaves web and ml alone.
`tests/test_artifact_paths.py` reads each Dockerfile and asserts every COPY
source is covered, so adding a COPY without updating the script fails CI.
- **A file that DECIDES an artifact's identity belongs in its set even though it
is copied into nothing** — `packaging.sh` for the extension and web (#3156),
and `artifacts.sh` itself for web (#3202), which decides the `FC_VERSION`
baked into that image. Only web needs the second entry: every artifact stamps
a revision, but a revision has a backstop (a changed derivation stops matching
the published label and forces a rebuild) and a version has none, because
nothing compares it to anything. `tests/test_artifact_paths.py`'s `DERIVERS`
table is the guard.
- **Builds are skipped when the content is already published.** Each image
carries its revision as an `fc.revision` LABEL, and `build.yml` reads that
label back off the moving channel tag (`imagetools inspect --format`). Equal
to the derived revision means the bytes are already published, so the job
repoints the remaining tags at the existing manifest instead of rebuilding.
Two things this depends on: an inspect that errors for ANY reason reads as a
MISS so no needed build is ever skipped, and the repoint must EXCLUDE the
source tag — `imagetools create` wraps its source in a manifest index, and
config labels do not resolve through an index, so writing the channel tag
from itself destroys the label the next run reads (#3183).
- **The build pushes exactly ONE tag — the channel's — and every other tag is
written registry-side afterwards** (#3190). buildx on this runner pushes the
first tag to the registry and then re-pushes the rest through the docker
driver, out of a local image store that a registry-direct build never fills;
it fails intermittently with `tag does not exist`. On `dev` that only reddens
a job, but on `main` it silently skips `:c-<sha>` while `:latest` publishes
fine — a missing rollback tag has no consumer that fails, so nothing but the
red job would notice until somebody needs to roll back. `imagetools create`
has no local store to be absent from, and it is the code the reuse path
already ran, so both paths now share one proven route. The cost: `:c-<sha>`
is an index rather than a plain image, so `fc.revision` does not resolve
through it — nothing reads it there, and the index names the same manifest.
- **`FC_CHANNEL` and `FC_VERSION` are build args, not runtime settings.**
`build.yml` passes them to the web image only — the ml and agent images have
nothing to report them to. `/api/health` returns both, the foot of Settings
renders them, and `/api/extension/manifest` reports the channel beside the
extension version so an install can be traced to a channel. With no version image tags, that
self-report is the ONLY answer to "which build is this?" — which is why a
missing version renders `unknown` rather than a blank: an empty footer reads
as "no version", a different and false claim.
Both are declared LAST in the Dockerfile on purpose: an ARG invalidates every
layer below it, and these are the values that differ between the dev and main
builds of identical source, so placing them earlier would stop the two
channels ever sharing a cached `pip install`. Empty by default — a local build
then reports nothing rather than claiming a channel it is not on.
- **The channel is never folded into the version.** A `-dev` suffix makes the
extension's per-segment `parseInt` comparator read that segment as 0, so every
dev build compares equal to every other — issue #2993 exactly (rule 149).
`frontend/test/systemBuild.spec.js` pins the rendered version to the bare
number.
TIME of the newest packaged-extension change (minutes since 2020-01-01, per
family rule 149 — never a commit count, which orders by branch rather than by
recency). `build.yml`'s `sign-extension` computes it and stamps it into
`extension/manifest.json` + `package.json` in the working tree before signing;
the stamp is never committed. Treat the version in the repo as a base: only
its MAJOR.MINOR is read, and its patch component is inert.
- Every job that calls `packaging.sh version` checks out with `fetch-depth: 0`
`build.yml`'s `sign-extension` and `build-web`, and `ci.yml`'s
`extension-version`. A depth-1 clone sees one commit and derives a wrong,
too-low value **rather than failing**, so the full-history checkout is
load-bearing rather than incidental.
- **`FC_CHANNEL` is a build arg, not a runtime setting.** `build.yml` passes
`dev` / `main` to the web image only (the ml and agent images have nothing to
report it to), and `/api/extension/manifest` reports it beside the version so
an install can be traced to a channel. It is declared LAST in the Dockerfile
on purpose: an ARG invalidates every layer below it, and this is the one value
that differs between the dev and main builds of identical source, so placing
it earlier would stop the two channels ever sharing a cached `pip install`.
Empty by default — a local build then reports no channel at all rather than
claiming one.
- Callers MUST `set -f` before substituting the script's output. Without it the
shell expands `test/**` against the working tree and silently narrows the
pattern to whatever files exist at that moment — a failure that looks like
+9 -24
View File
@@ -38,42 +38,27 @@ npm run build # unsigned XPI in web-ext-artifacts/
- [ ] Subscriptions list: popup → "Sources" tab → list renders
- [ ] Check now: click play icon on source row → no error toast
## Versioning — the committed number decides nothing
## Versioning — don't hand-edit the patch number
The shipped version is **derived**, not committed. `scripts/packaging.sh
version` returns `YYYY.M.D.HHMM` in UTC: the commit *time* of the newest change
to a packaged extension file. `build.yml` computes it and stamps it into both
version` returns `MAJOR.MINOR` from `manifest.json` plus a patch component
that is the commit *time* of the newest change to a packaged extension file,
in minutes since 2020-01-01. `build.yml` computes it and stamps it into both
`manifest.json` and `package.json` at build time. The stamp is never
committed — the commit carrying it would itself be a change to the extension,
which would move the version again.
So:
- **Editing the version does nothing.** All of it is overwritten before web-ext
ever reads it. There is no bump to make, and none to forget. There is no
hand-set part left either: MAJOR.MINOR went away with milestone 318 step 8.
- **Editing the patch number does nothing.** It is overwritten before web-ext
ever reads it. There is no bump to make, and none to forget.
- **MAJOR.MINOR is still yours.** It carries the deliberate meaning, it is read
from `manifest.json` alone, and CI fails the `extension-version` lane if the
two files disagree on it.
- `npm run build` locally produces an XPI labelled with the *committed*
version, since nothing stamped it. Fine for loading into a test profile; not
what ships.
**Why the extension is the one artifact that does not zero-pad.** Every other
FC artifact emits rule 148's `YYYY.MM.DD.HHMM`. AMO will not take it: Mozilla's
grammar for addons.mozilla.org is
```
^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
```
— each segment is the single digit `0` or starts 1-9, so `08` and `0201` are
rejected, and at most four segments are allowed. The extension therefore emits
**the same numbers unpadded**: `2026.8.29.201` where the rest of the family
says `2026.08.29.0201`. Rule 148 already defines comparison as numeric per
segment, under which the two are equal, so nothing is reordered by the choice
and left-padding each segment recovers the family string exactly. `ci.yml`'s
`extension-version` lane checks the derived string against that regex on every
push — the cheap place to find out, because AMO 409s on re-signing and a
rejected version is burned for good.
Why commit time and not a commit count: a count is per-branch, so `dev` and
`main` count different histories of the same code and their versions end up
ordered by which branch accumulated more commits rather than by which is newer.
+41 -61
View File
@@ -60,7 +60,7 @@ NOT_PACKAGED_BUILD='web-ext-artifacts node_modules'
NOT_VERSION_RELEVANT='package.json package-lock.json README.md .gitignore vitest.config.js test test/**'
usage() {
echo "usage: packaging.sh {ignore|pathspec|version}" >&2
echo "usage: packaging.sh {ignore|pathspec|version|major-minor|patch}" >&2
exit 2
}
@@ -86,48 +86,36 @@ cmd_pathspec() {
echo
}
# Strip leading zeros from one segment, leaving at least one digit.
#
# This exists for AMO and nothing else. Mozilla's version grammar for
# addons.mozilla.org is documented as
#
# ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
#
# — each segment is either the single digit `0` or starts 1-9, so `08` and
# `0201` are rejected outright, while `0` itself is fine. MDN states it in
# prose too: "Non-zero numbers must not include a leading zero."
#
# POSIX sh has no trim-loop, hence the while.
unpad() {
s=$1
while [ "${#s}" -gt 1 ]; do
case "$s" in
0*) s=${s#0} ;;
*) break ;;
esac
done
printf '%s' "$s"
# MAJOR.MINOR stays hand-set in manifest.json — it's the part that carries
# deliberate meaning. Only the patch component is derived.
cmd_major_minor() {
root=$(git rev-parse --show-toplevel)
grep -E '"version"' "$root/extension/manifest.json" \
| head -1 \
| sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([0-9]+)\.([0-9]+).*/\1.\2/'
}
# The extension's version: `YYYY.M.D.HHMM`, UTC, derived from the commit TIME
# of the newest change to a PACKAGED extension file.
# 2020-01-01T00:00:00Z — the anchor for the derived patch component. Fixed
# forever; moving it would renumber every version downwards.
VERSION_EPOCH=1577836800
# Minutes since VERSION_EPOCH of the LATEST commit that touched a PACKAGED
# extension file.
#
# THE ONE DELIBERATE DEPARTURE FROM THE FAMILY SHAPE, and it is a rendering
# difference only. Rule 148 says `YYYY.MM.DD.HHMM` zero-padded, and every other
# FC artifact emits exactly that. AMO's grammar (see unpad) forbids the padding,
# and AMO is not negotiable: a rejected version is burned, since AMO 409s on
# re-signing a version it has already seen. So the extension emits THE SAME
# NUMBERS unpadded — 2026.08.29.0201 and 2026.8.29.201 are one value in two
# renderings, and rule 148 already specifies comparison as numeric per segment,
# under which they are equal. Nothing published is reordered by the choice, and
# left-padding each segment recovers the family string exactly.
#
# HHMM is one segment, not two, because AMO allows at most FOUR. Unpadded that
# reads oddly (00:14 -> `14`, midnight -> `0`) but stays strictly increasing
# within a day, which is all the ordering needs.
# Time-derived, per family rule 149: an artifact's ordering key must never be a
# commit count. A count is per-branch — `dev` and `main` count different
# histories of the same code — so the moment BOTH channels publish, their
# versions order by which branch accumulated more commits rather than by which
# is newer. A squash-merge makes that permanent: main gains one commit where dev
# gained five, so dev climbs away from main and a dev install can never cross
# back. That is Roundtable's 2026-08-24 incident (`versionCode` was the branch's
# commit count) in a different repo. Measured here on 2026-08-27: main=23,
# dev=24 under the old formula — one apart, which is exactly how the inversion
# stays invisible until it strands somebody.
#
# Why the commit's time and not the build's:
# * MONOTONIC — max() over a set that only ever gains members.
# * MONOTONIC — max() over a set that only ever gains members. Verified
# across all 24 extension-touching commits: zero non-monotonic steps.
# * STABLE while the extension is unchanged, so an unchanged extension keeps
# its version, the ext-<version> signature cache still hits, and AMO is
# called once per extension CHANGE rather than once per push. Build-time
@@ -138,40 +126,32 @@ unpad() {
# produced for byte-identical code. Same code, same version, one signing.
# * REPRODUCIBLE — any checkout of a commit yields that commit's version.
#
# Never a commit count (family rule 149): a count is per-branch, so `dev` and
# `main` count different histories of the same code and order by which branch
# accumulated more commits rather than by which is newer. A squash-merge makes
# that permanent. Roundtable's 2026-08-24 incident, in a different repo.
#
# Requires real history: a depth-1 clone sees one commit and will derive a wrong
# (too low) value. Every consumer must check out with fetch-depth: 0.
#
# Formatted through git rather than date(1): busybox date does not reliably
# accept `-d @<epoch>`, and git's --date=format-local is available wherever git
# is. TZ=UTC so the value does not depend on the runner's timezone.
cmd_version() {
cmd_patch() {
root=$(git rev-parse --show-toplevel)
# Unquoted on purpose: the pathspec must word-split into separate args.
# Globbing is already off script-wide (set -euf above).
# shellcheck disable=SC2046
sha=$(cd "$root" && git log --format='%ct %H' HEAD -- extension/ $(cmd_pathspec) \
| sort -n | tail -1 | cut -d' ' -f2)
if [ -z "$sha" ]; then
ts=$(cd "$root" && git log --format=%ct HEAD -- extension/ $(cmd_pathspec) \
| sort -n | tail -1)
if [ -z "$ts" ]; then
echo "packaging.sh: no commit touches a packaged extension file" >&2
exit 1
fi
padded=$(cd "$root" && TZ=UTC git show -s --format=%cd \
--date='format-local:%Y.%m.%d.%H%M' "$sha")
# Rebinding the function's own positional params, which are unused here.
# shellcheck disable=SC2046
set -- $(echo "$padded" | tr '.' ' ')
echo "$(unpad "$1").$(unpad "$2").$(unpad "$3").$(unpad "$4")"
echo $(( (ts - VERSION_EPOCH) / 60 ))
}
cmd_version() {
echo "$(cmd_major_minor).$(cmd_patch)"
}
[ $# -ge 1 ] || usage
case "$1" in
ignore) cmd_ignore ;;
pathspec) cmd_pathspec ;;
version) cmd_version ;;
*) usage ;;
ignore) cmd_ignore ;;
pathspec) cmd_pathspec ;;
version) cmd_version ;;
major-minor) cmd_major_minor ;;
patch) cmd_patch ;;
*) usage ;;
esac
+26 -40
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { readdirSync, readFileSync } from 'node:fs'
import { readFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
@@ -8,10 +8,10 @@ const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
const read = (name) => JSON.parse(readFileSync(path.join(EXT_DIR, name), 'utf8'))
const readText = (...seg) => readFileSync(path.join(EXT_DIR, ...seg), 'utf8')
// Only the git-free subcommands are exercised here: `version` shells out to
// git, and the extension lane runs on node:24-bookworm-slim which may not ship
// it. That one is covered where git is guaranteed — ci.yml's extension-version
// lane and build.yml both run on ci-python.
// Only the git-free subcommands are exercised here: `version`/`patch` shell out
// to git, and the extension lane runs on node:24-bookworm-slim which may not
// ship it. Those two are covered where git is guaranteed — ci.yml and build.yml
// run on ci-python.
const packaging = (cmd) =>
execFileSync('sh', [path.join(EXT_DIR, 'scripts', 'packaging.sh'), cmd], {
cwd: EXT_DIR,
@@ -107,24 +107,15 @@ describe('consumers delegate rather than keeping their own copy', () => {
}
})
// Read from disk rather than listed by hand. The point of this assertion is
// that it survives consumers coming and going, and a hardcoded list is the
// one part of it that cannot — release.yml (milestone 318 step 7) would have
// joined the directory without joining the check.
const WORKFLOWS = readdirSync(path.join(EXT_DIR, '..', '.forgejo', 'workflows')).filter((f) =>
f.endsWith('.yml')
)
const WORKFLOWS = ['ci.yml', 'build.yml', 'extension.yml']
it('no workflow hardcodes the packaged-file set', () => {
// ci.yml used to substitute `packaging.sh pathspec` directly, for the
// manual-bump guard that milestone 271 step 5 retired. Nothing inlines the
// set today, and nothing should start to: a literal :(exclude)extension/...
// in a workflow means someone bypassed the shared definition, which is
// exactly the drift #2397 was about.
expect(
WORKFLOWS.length,
'no workflows found — the glob is not looking where it thinks'
).toBeGreaterThan(2)
// exactly the drift #2397 was about. Asserted across all three rather than
// against one named consumer, so it keeps holding as consumers come and go.
for (const wf of WORKFLOWS) {
const text = readText('..', '.forgejo', 'workflows', wf)
expect(text, `${wf} inlines an :(exclude) literal`).not.toMatch(/:\(exclude\)extension\//)
@@ -145,33 +136,28 @@ describe('consumers delegate rather than keeping their own copy', () => {
})
describe('extension version', () => {
// Mozilla's published grammar for addons.mozilla.org, transcribed from MDN's
// manifest.json/version page. Each segment is the single digit 0 or starts
// 1-9 — so no leading zeros — and there are at most four of them.
const AMO = /^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$/
const majorMinor = (v) => v.split('.').slice(0, 2).join('.')
it('keeps a committed version AMO would accept, though it ships nothing', () => {
// The committed value is wholly inert since milestone 318 step 8: there is
// no hand-set MAJOR.MINOR left for packaging.sh to read, and build.yml
// stamps the derived string over both files before web-ext sees them.
it('keeps the hand-set MAJOR.MINOR in lockstep across both files', () => {
// Narrowed from full-string equality at milestone 271 step 5. Since step 4
// the patch component is derived from commit time and stamped into both
// files at build time, so the committed patch numbers are inert — nothing
// reads them and they are not what ships. Asserting on them would fail for
// a difference that changes nothing.
//
// It is still asserted, for one reason: `npm run build` locally packages
// whatever is committed, so a value AMO would reject turns a local build
// into a confusing failure with no CI signal ahead of it. ci.yml checks
// the same grammar against the DERIVED value, which is the one AMO sees.
for (const file of ['manifest.json', 'package.json']) {
expect(read(file).version, `${file} version is not AMO-shaped`).toMatch(AMO)
}
// MAJOR.MINOR is the opposite: still hand-set, still shipped, and
// packaging.sh reads it from manifest.json ALONE. Let the two diverge and
// the extension ships a version package.json disagrees with, with no other
// signal.
expect(majorMinor(read('manifest.json').version))
.toBe(majorMinor(read('package.json').version))
})
it('rejects the zero-padded family shape, which is why the extension unpads', () => {
// Guards the reason for the exception, not just its result. If this ever
// starts passing, someone has loosened the pattern and the next sign burns
// an AMO version to find out. (#3138.)
expect('2026.08.29.0201').not.toMatch(AMO)
expect('2026.8.29.201').toMatch(AMO)
// Five segments: AMO allows four.
expect('2026.8.29.2.1').not.toMatch(AMO)
it('uses a plain dotted numeric version AMO will accept', () => {
// The committed value seeds MAJOR.MINOR, so it still has to parse even
// though its patch component never ships. ci.yml asserts the same shape on
// the DERIVED value, which is the one AMO actually sees.
expect(read('package.json').version).toMatch(/^\d+(\.\d+)*$/)
})
it('declares manifest v3', () => {
+1 -25
View File
@@ -5,18 +5,6 @@ import { useApi } from '../composables/useApi.js'
export const useSystemStore = defineStore('system', () => {
const api = useApi()
const healthy = ref(null) // null=unknown, true=ok, false=down
// What the instance says it is. Since milestone 318 stopped publishing
// version image tags, this is the only answer to "which build is this?" —
// there is no registry name left to check it against.
//
// Three states, and collapsing any two of them would lie:
// buildLoaded=false we have not asked yet -> render nothing
// buildLoaded=true, version='' the build cannot say -> render "unknown"
// buildLoaded=true, version=x this build is x
// A blank footer would read as "no version", which is a different claim.
const buildVersion = ref('')
const buildChannel = ref('')
const buildLoaded = ref(false)
const stats = ref(null)
const statsLoading = ref(false)
@@ -24,17 +12,8 @@ export const useSystemStore = defineStore('system', () => {
try {
const body = await api.get('/api/health')
healthy.value = body.status === 'ok'
// Absent means "cannot say" — the server omits these rather than
// sending empty strings, so `?? ''` preserves that rather than
// inventing a value for it.
buildVersion.value = body.version ?? ''
buildChannel.value = body.channel ?? ''
buildLoaded.value = true
} catch {
healthy.value = false
// Deliberately NOT setting buildLoaded: a failed health call tells us
// nothing about the build, and claiming "unknown" would present a
// network blip as a defective image.
}
}
@@ -47,8 +26,5 @@ export const useSystemStore = defineStore('system', () => {
}
}
return {
healthy, stats, statsLoading, refreshHealth, refreshStats,
buildVersion, buildChannel, buildLoaded,
}
return { healthy, stats, statsLoading, refreshHealth, refreshStats }
})
-15
View File
@@ -54,21 +54,6 @@
<MaintenancePanel />
</v-window-item>
</v-window>
<!-- Which build is this? With no version image tags (milestone 318) the
instance's own report is the only answer, so it is shown rather than
hidden. The instinct to treat it as information disclosure does not
survive contact: the JS bundle and asset hashes fingerprint the build
anyway, and "I'm on 2026.08.28.1249" is the single most useful line in
a bug report.
Channel sits BESIDE the version, never inside it (rule 149) — a
`-dev` suffix would read as a 0 segment to the extension's comparator
and make every dev build compare equal (#2993). -->
<div v-if="system.buildLoaded" class="text-caption text-medium-emphasis text-center mt-8">
FabledCurator {{ system.buildVersion || 'unknown' }}
<span v-if="system.buildChannel"> · {{ system.buildChannel }}</span>
</div>
</v-container>
</template>
-96
View File
@@ -1,96 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useSystemStore } from '../src/stores/system.js'
// Which build am I running? Milestone 318 stopped publishing version image
// tags, so the instance's own report is the ONLY answer — there is no registry
// name left to check it against. That promotes this from a convenience to the
// mechanism, and it means the three states below have to stay distinct: a
// wrong answer here has nothing to contradict it.
//
// not asked yet -> render nothing
// asked, no version -> render "unknown"
// asked, has a version -> render it
//
// Collapsing the first two would show "unknown" during every page load, and
// collapsing either into a blank would read as "no version", which is a
// different and false claim.
function stubHealth(body, { fail = false } = {}) {
globalThis.fetch = vi.fn(async () => {
if (fail) throw new Error('network down')
return {
ok: true, status: 200, statusText: '200',
text: async () => JSON.stringify(body),
}
})
}
describe('system store — build identity', () => {
beforeEach(() => setActivePinia(createPinia()))
afterEach(() => { vi.restoreAllMocks(); delete globalThis.fetch })
it('starts having asked nothing, so the footer renders nothing', () => {
const s = useSystemStore()
expect(s.buildLoaded).toBe(false)
})
it('reports the version and channel the instance claims', async () => {
stubHealth({ status: 'ok', version: '2026.08.28.1249', channel: 'dev' })
const s = useSystemStore()
await s.refreshHealth()
expect(s.buildLoaded).toBe(true)
expect(s.buildVersion).toBe('2026.08.28.1249')
expect(s.buildChannel).toBe('dev')
})
it('keeps the channel OUT of the version string', async () => {
// The tempting shortcut is a `-dev` suffix. The extension's comparator
// parses each dotted segment with parseInt, so a suffixed segment reads as
// 0 and every dev build compares equal to every other — #2993 exactly
// (rule 149). If anyone ever "simplifies" by folding them together, the
// version stops being the bare derived number and this fails.
stubHealth({ status: 'ok', version: '2026.08.28.1249', channel: 'dev' })
const s = useSystemStore()
await s.refreshHealth()
expect(s.buildVersion).toBe('2026.08.28.1249')
expect(s.buildVersion).not.toContain('dev')
})
it('treats an absent version as "cannot say", not as a value', async () => {
// A locally-built image, or one predating the field. The server omits the
// key rather than sending an empty string; `?? ''` must preserve that
// rather than inventing something. The view renders "unknown" from it.
stubHealth({ status: 'ok' })
const s = useSystemStore()
await s.refreshHealth()
expect(s.buildLoaded).toBe(true)
expect(s.buildVersion).toBe('')
expect(s.buildChannel).toBe('')
})
it('reports a version with no channel without inventing one', async () => {
stubHealth({ status: 'ok', version: '2026.08.28.1249' })
const s = useSystemStore()
await s.refreshHealth()
expect(s.buildVersion).toBe('2026.08.28.1249')
expect(s.buildChannel).toBe('')
})
it('does not claim "unknown" when the health call itself failed', async () => {
// A network blip says nothing about the build. Marking it loaded here
// would present a transient failure as a defective image — and since
// nothing else names the build, there would be no second source to
// correct the impression.
stubHealth(null, { fail: true })
const s = useSystemStore()
await s.refreshHealth()
expect(s.healthy).toBe(false)
expect(s.buildLoaded).toBe(false)
})
})
+88 -83
View File
@@ -3,11 +3,6 @@
# version derived from it. Milestone 313; generalises the shape
# extension/scripts/packaging.sh established for the extension alone.
#
# "Built from" is deliberately wider than "copied into". A file that DECIDES an
# artifact's identity is part of what that artifact is built from even though it
# never reaches the image — see DERIVER below, and #3156 for the same finding
# about packaging.sh.
#
# Four artifacts, four independent versions. An artifact whose shipped files
# did not change keeps its version and does not rebuild — that is the whole
# point, and it is why each path set must match its Dockerfile rather than
@@ -61,36 +56,25 @@ ML_PATHS='Dockerfile.ml requirements-ml.txt requirements.txt backend alembic ale
# it: this is deliberately NOT `agent/`.
AGENT_PATHS='agent/Dockerfile agent/requirements.txt agent/fc_agent'
# This file. It is copied into no image and it is still part of what the web
# image is built from, because it DECIDES the FC_VERSION baked into that image
# (#3202). Same finding as #3156 about packaging.sh, one level up.
# Which artifacts bake the BUILD CHANNEL into the image, and therefore cannot
# share a content identity across channels. The web image takes FC_CHANNEL as
# a build-arg and reports it from /api/extension/manifest (milestone 271 step
# 7), so `main` and `dev` builds of one revision are genuinely different
# images — reusing the dev one on main would ship an instance that names
# itself `dev` forever.
#
# Why web and nothing else. Every artifact stamps `fc.revision`, but only web
# also stamps a version (build.yml line ~488 feeds `version web` to the
# FC_VERSION build arg; ml and agent ask for `revision` alone, and the
# extension takes its version from packaging.sh). For a revision-only artifact
# this file needs no entry: any change to how the revision is COMPUTED changes
# the derived value, which then disagrees with the label on the published image
# and forces a rebuild. That mechanism is self-correcting because it compares
# against a string stamped into a real artifact.
# ml and agent take no build-args at all: one revision, one image, and a merge
# to main can reuse exactly what dev already built. That is not a detail, it is
# most of what step 4 saves — merges would otherwise rebuild the agent's CUDA
# image to produce bytes that already exist.
#
# The version is compared against nothing, so it has no such backstop. Before
# this entry, a change to cmd_version alone left every artifact's revision
# untouched, the reuse check hit, the build was skipped, and the published
# image went on reporting the OLD version format — silently, until some
# unrelated commit happened to force a rebuild. Milestone 318 step 5 is the
# worked instance: b3989d0 and 5771fd5 share revision fb2c4d5b80be while the
# version moved 2026.8.28.1249 -> 2026.08.28.1249. It cost nothing only because
# FC_VERSION did not exist until one commit later.
#
# Named as a file, not as `scripts`: release_notes.py lives beside it and only
# READS derived values, so it decides nothing and must not re-version anything.
# A future script that derives an identity belongs here explicitly.
DERIVER='scripts/artifacts.sh'
# Extend this list if a second artifact ever gains a build-arg;
# tests/test_artifact_identity.py reads the Dockerfiles and fails if it drifts.
CHANNELLED='web'
usage() {
echo "usage: artifacts.sh {paths|revision|version} {web|ml|agent|extension}" >&2
echo "usage: artifacts.sh {paths|revision|version|tag} {web|ml|agent|extension}" >&2
echo " artifacts.sh identity {web|ml|agent} [channel]" >&2
exit 2
}
@@ -103,7 +87,7 @@ ext_paths() {
cmd_paths() {
case "$1" in
web) echo "$WEB_PATHS $DERIVER $(ext_paths)" ;;
web) echo "$WEB_PATHS $(ext_paths)" ;;
ml) echo "$ML_PATHS" ;;
agent) echo "$AGENT_PATHS" ;;
extension) ext_paths ;;
@@ -132,64 +116,83 @@ fmt() {
(cd "$ROOT" && TZ=UTC git show -s --format=%cd --date="format-local:$2" "$1")
}
# Leading zeros stripped so every segment is a plain integer — some version
# validators reject `08`, and a leading zero buys nothing. `0000` (midnight)
# must survive as `0`, not as the empty string.
strip0() {
printf '%s' "$1" | sed -e 's/^0*//' -e 's/^$/0/'
}
# The IDENTITY of an artifact's content: the commit its shipped files last
# changed in. This is what decides whether a build can be skipped.
#
# It is published as the `fc.revision` LABEL on the image itself, and read
# back off the moving channel tag — not as a tag of its own (milestone 318
# step 3). A tag would be a name minted per build that only one thing reads,
# which is what rule 145 narrowed against; it would also be prunable under the
# registry's keep_pattern (#3157), so the cache would silently expire.
#
# A published image with no such label reads as a MISS and rebuilds. That is
# the migration path, not a fault: `imagetools create` copies a manifest and
# config labels are not manifest annotations, so the reuse path cannot stamp
# one and there is nothing to backfill. Each artifact pays one rebuild, once.
# changed in. This — not the tag — is what decides whether a build can be
# skipped, because the published tag is only day-precise and two different
# builds can share it.
cmd_revision() {
echo "$(newest "$1")" | cut -d' ' -f2 | cut -c1-12
}
# The VERSION: `YYYY.MM.DD.HHMM`, zero-padded, UTC. One shape across the whole
# family (note #3127 §1, rule 148) — the number an instance reports about
# itself, and, with a `v` in front, the release tag naming the same build.
#
# Zero-padded since 2026-08-28. This stripped leading zeros until then, on the
# reasoning that every segment should read as a plain integer — which never
# held, since comparison strips them on parse anyway. Padding costs nothing,
# sorts lexically as well as numerically, and keeps this project emitting the
# same string as its siblings: unpadded, a `2026.8.28.1432` here sits beside a
# `2026.08.28.1432` there, two shapes one character apart. Two obviously
# different formats are safer than two nearly identical ones.
#
# Comparison is numeric per dot-segment, so `08` and `8` are equal and nothing
# already published is reordered by the change.
#
# HHMM is not decoration: it is what makes the value unique per build with no
# lookup. A date alone collides on the second build of a day, and resolving
# that needs a `.N` suffix, which needs asking the registry what already
# exists — at which point two lanes derive different answers for one source
# and the shared-signature property is lost.
# The ORDERING KEY: full precision, YYYY.M.D.HHMM. Used by the extension,
# where the value is what Firefox compares to decide whether an update exists
# — two same-day builds MUST be distinguishable or the second never reaches
# anyone.
cmd_version() {
# The extension is the one artifact this script does not FORMAT, only route.
# AMO's version grammar forbids leading zeros, so the extension emits the
# same numbers unpadded (#3138) — a rendering exception, documented in
# packaging.sh beside the signing step that has to obey it. Delegating keeps
# one answer per artifact: `artifacts.sh version extension` and
# `packaging.sh version` cannot drift into two.
#
# The direction is deliberate. artifacts.sh already asks packaging.sh for the
# extension's PATH SET (ext_paths above), so the version has to flow the same
# way; reversing it would have packaging.sh call back into this script, which
# would call packaging.sh for the paths again.
if [ "$1" = extension ]; then
sh "$ROOT/extension/scripts/packaging.sh" version
return
fi
sha=$(echo "$(newest "$1")" | cut -d' ' -f2)
# One git call for the whole string rather than four and a sed. git's
# format-local takes the complete format, and doing it in pieces was only
# ever there to strip the padding between them.
fmt "$sha" '%Y.%m.%d.%H%M'
printf '%s.%s.%s.%s\n' \
"$(fmt "$sha" %Y)" \
"$(strip0 "$(fmt "$sha" %m)")" \
"$(strip0 "$(fmt "$sha" %d)")" \
"$(strip0 "$(fmt "$sha" %H%M)")"
}
# The PUBLISHED IMAGE TAG: day precision, YYYY.M.D. Deliberately coarser than
# the ordering key, per the operator 2026-08-28 — same-day work is not
# something worth pinning, so a second build the same day replaces the first
# rather than accumulating a tag nobody would roll back to. Safe only because
# skip decisions key on cmd_revision, never on this.
cmd_tag() {
sha=$(echo "$(newest "$1")" | cut -d' ' -f2)
printf '%s.%s.%s\n' \
"$(fmt "$sha" %Y)" \
"$(strip0 "$(fmt "$sha" %m)")" \
"$(strip0 "$(fmt "$sha" %d)")"
}
# The CONTENT IDENTITY of a published image: an immutable tag naming exactly
# what a build of this commit would produce. build.yml asks the registry for it
# and, on a hit, skips the build entirely and repoints the channel and date
# tags at the manifest that is already there (milestone 313 step 4).
#
# It is deliberately NOT either of the other two values:
# * the date tag is day-precise and last-one-wins, so two different builds
# share it — it cannot answer "is this content published?".
# * the commit sha moves on every push, so it would never hit, which is the
# redundant rebuild this exists to remove.
#
# The revision does both jobs: it is content-unique AND stable across pushes
# that did not touch the artifact.
cmd_identity() {
_art=$1
_chan=${2:-}
case "$_art" in
web|ml|agent) ;;
extension)
echo "artifacts.sh: the extension is cached as an ext-<version> Forgejo release, not an image tag — use \`version\`" >&2
exit 2 ;;
*) usage ;;
esac
for _c in $CHANNELLED; do
if [ "$_art" = "$_c" ]; then
# Refused rather than defaulted: an unqualified identity for a
# channelled artifact would let a dev image be reused as the main one.
if [ -z "$_chan" ]; then
echo "artifacts.sh: $_art bakes the channel into the image — identity needs one" >&2
exit 2
fi
printf 'r-%s-%s\n' "$(cmd_revision "$_art")" "$_chan"
return
fi
done
printf 'r-%s\n' "$(cmd_revision "$_art")"
}
[ $# -ge 2 ] || usage
@@ -197,5 +200,7 @@ case "$1" in
paths) cmd_paths "$2" ;;
revision) cmd_revision "$2" ;;
version) cmd_version "$2" ;;
tag) cmd_tag "$2" ;;
identity) cmd_identity "$2" "${3:-}" ;;
*) usage ;;
esac
-312
View File
@@ -1,312 +0,0 @@
"""Publish a Forgejo release whose body is derived from git, not written by hand.
Milestone 318 step 2 took the build consequence away from a `v*` tag: `main`
has already built and published the commit by the time anyone tags it, and
rebuilding would re-push `:c-<sha>`, which rule 145 forbids even when the bytes
match. That left the tag with nothing to do. This gives it the job it has left.
**The half of the question a version string cannot answer.** Step 6 puts
`2026.08.28.2208` in the Settings footer, so an operator can say which build
they are running. They still cannot say what is in it that was not in the one
they ran last month. A dated release carrying the commits since the previous
one is the object that interprets the identifier (note #3127 §5).
**Derived, so it cannot drift.** The alternative is a hand-maintained
`CHANGELOG.md`, which goes aspirational the first time someone forgets — and
nothing ever catches it, because there is no second source to disagree with.
Every line below comes out of `git log` at publish time.
**Optional by construction.** Release tags are bookmarks: cut one when you will
want to point at that day by name, otherwise don't. FC went twelve weeks
without one and nothing was wrong (note #3127 §0). This runs on a tag push and
on nothing else — deliberately no schedule and no auto-tag on merge, either of
which would turn an optional bookmark back into ceremony.
## Finding the previous release
`git describe --exclude <this tag>`, which walks ANCESTRY, not a sorted list.
That is not fussiness: this repo's existing tags are the old `v26.05.22.0`
shape and the next one will be rule 148's `v2026.08.28.2208`. Lexicographically
`v2026...` sorts BEFORE `v26...` — every release from here on would report its
predecessor as itself-or-nothing and emit a changelog covering the entire
history. Ancestry is immune to the shape change, and it is also the more honest
question: "what is in this that was not in the last one" IS a reachability
question.
## Re-runs update, they do not fall through
Note #3127 §6.7: a publisher that POSTs and recovers the id from a `409` never
rewrites the body, so a re-run silently keeps the first version. Harmless for a
`v*` tag created once — and wrong the moment anything re-points. This one GETs
first and PATCHes when the release exists, so it is correct either way rather
than correct by luck (ThoughtSync #2182 is the same bug).
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
API = "https://git.fabledsword.com/api/v1/repos/bvandeusen/FabledCurator"
IMAGES = (
"git.fabledsword.com/bvandeusen/fabledcurator",
"git.fabledsword.com/bvandeusen/fabledcurator-ml",
"git.fabledsword.com/bvandeusen/fabledcurator-agent",
)
# Rule 148: `v` + the artifact's own version, zero-padded, no `.N`, no lookup.
RULE_148 = re.compile(r"^v\d{4}\.\d{2}\.\d{2}\.\d{4}$")
# Past this, the list has stopped being something anyone reads. It is reached
# in exactly one situation — no previous tag is reachable, so the span is the
# whole history — which happens on a genuine first release and on a tag cut
# somewhere `main`'s tags cannot be seen from. Truncating says so; emitting
# 1100 lines would bury the note explaining why there are 1100 of them.
MAX_COMMITS = 200
def git(*args: str) -> str:
return subprocess.run(
["git", *args], capture_output=True, text=True, check=True
).stdout.strip()
def git_ok(*args: str) -> str | None:
"""Run git, returning None instead of raising when it fails.
Used for the questions that legitimately have no answer — no previous tag,
no local `main` — where the absence is information rather than a fault.
"""
try:
return git(*args)
except subprocess.CalledProcessError:
return None
def previous_tag(ref: str, tag: str | None) -> str | None:
"""The most recent `v*` tag reachable from `ref`, excluding `tag` itself.
`--exclude` rather than `<ref>^` so this is the same call whether or not
`ref` is the tag being released — and so it does not blow up on a root
commit that has no parent to walk to.
"""
args = ["describe", "--tags", "--abbrev=0", "--match", "v*"]
if tag:
args += ["--exclude", tag]
return git_ok(*args, ref)
def commits(previous: str | None, ref: str) -> list[str]:
"""The subjects between the previous release and this one.
`--no-merges` because rule 153 merges `dev` into `main` with a plain merge
commit, so `main`'s first-parent view is a list of "Merge pull request #N"
and nothing else. The work is in the commits under those merges.
"""
span = f"{previous}..{ref}" if previous else ref
out = git("log", "--no-merges", "--format=%s (%h)", span)
return [line for line in out.split("\n") if line.strip()]
def truncate(log: list[str]) -> tuple[list[str], str | None]:
if len(log) <= MAX_COMMITS:
return log, None
return log[:MAX_COMMITS], (
f"{len(log)} commits in this span — more than a changelog is for. "
f"Listing the newest {MAX_COMMITS}. This usually means no previous "
f"`v*` tag was reachable from here."
)
def render(tag: str, sha: str, previous: str | None, log: list[str], notes: list[str]) -> str:
short = sha[:7]
parts = []
if notes:
# Anything the derivation could not stand behind goes at the TOP, not
# in a footnote. A release that quietly names a build nobody can find
# is the failure this whole milestone is about.
parts.append("\n".join(f"> **Note:** {n}" for n in notes))
parts.append(
f"Built from `{short}`. The rollback unit is the immutable `:c-` tag "
f"(rule 145) — these three move together:\n\n```\n"
+ "\n".join(f"{image}:c-{short}" for image in IMAGES)
+ "\n```"
)
heading = f"## Changes since {previous}" if previous else "## Changes"
if log:
parts.append(heading + "\n\n" + "\n".join(f"- {line}" for line in log))
else:
parts.append(
heading
+ "\n\n_No non-merge commits since the previous release. This tag "
"names the same source under a new name._"
)
span = f"{previous}..{tag}" if previous else tag
parts.append(
f"---\n\n_Derived at publish time from `git log --no-merges {span}`. "
f"Nothing here is hand-maintained._"
)
return "\n\n".join(parts)
def cross_checks(tag: str, sha: str) -> list[str]:
"""Everything the derivation knows that would make the release a lie.
Reported rather than enforced. The tag is already pushed by the time this
runs, so failing here would leave the operator with a tag and no release
and nothing but a red lane to explain it — while the release itself is
still the useful object. Say what is wrong, on the release, and publish.
"""
notes = []
if not RULE_148.match(tag):
notes.append(
f"`{tag}` is not rule 148's `vYYYY.MM.DD.HHMM` shape. Published "
f"anyway — the old `v26.*` tags predate the rule."
)
else:
derived = artifact_version("web")
if derived and derived != tag[1:]:
notes.append(
f"This tag names `{tag[1:]}`, but the web image built from "
f"`{sha[:7]}` reports `{derived}`. The Settings footer will not "
f"match this release's name."
)
# `:c-<sha>` only exists if `main` built this commit. Checking costs one
# git call; claiming it without checking costs a rollback that 404s at the
# moment someone needs it.
main = git_ok("rev-parse", "--verify", "-q", "refs/remotes/origin/main")
if main is None:
notes.append(
"Could not resolve `origin/main` here, so the `:c-` tags above are "
"unverified — they exist only if `main` built this commit."
)
elif subprocess.run(
["git", "merge-base", "--is-ancestor", sha, main], capture_output=True
).returncode != 0:
notes.append(
f"`{sha[:7]}` is not on `main`, so no `:c-{sha[:7]}` images were "
f"ever published. The refs above will not pull."
)
return notes
def artifact_version(artifact: str) -> str | None:
"""What `artifacts.sh` derives for one artifact in the CURRENT checkout.
It takes no ref because `artifacts.sh` takes none — it walks history from
HEAD. That is right here only because a tag push checks out the tagged
commit; calling this after `--dry-run some-other-ref` would compare the
tag against the working tree, which is why the mismatch note below is
reported and not enforced.
Returns None rather than raising if the script is missing or unhappy: a
cross-check that cannot run should not take the release down with it.
"""
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
try:
return subprocess.run(
["sh", os.path.join(root, "scripts", "artifacts.sh"), "version", artifact],
capture_output=True, text=True, check=True, cwd=root,
).stdout.strip()
except (subprocess.CalledProcessError, OSError):
return None
def api(method: str, path: str, token: str, payload: dict | None = None) -> dict | None:
body = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(
API + path, data=body, method=method,
headers={
"Authorization": "token " + token,
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
except urllib.error.HTTPError as exc:
if exc.code == 404:
return None
sys.exit(f"release: {method} {path} failed with HTTP {exc.code}: {exc.read()!r}")
def publish(tag: str, name: str, body: str, token: str) -> None:
existing = api("GET", f"/releases/tags/{tag}", token)
if existing:
api("PATCH", f"/releases/{existing['id']}", token, {"name": name, "body": body})
print(f"release: updated existing release {existing['id']} for {tag}")
else:
api("POST", "/releases", token, {"tag_name": tag, "name": name, "body": body})
print(f"release: created release for {tag}")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"ref", nargs="?", default=None,
help="tag or commit to release. Defaults to GITHUB_REF's tag, else HEAD.",
)
ap.add_argument(
"--dry-run", action="store_true",
help="render the body to stdout and publish nothing. Needs no token, "
"so it also works as a preview before you decide to cut the tag.",
)
args = ap.parse_args()
github_ref = os.environ.get("GITHUB_REF", "")
if args.ref:
ref = args.ref
elif github_ref.startswith("refs/tags/"):
ref = github_ref[len("refs/tags/"):]
else:
ref = "HEAD"
# A tag only if git knows it as one — `HEAD` and a raw sha are refs to
# release FROM, never the name to exclude or to publish under.
tag = ref if git_ok("rev-parse", "--verify", "-q", f"refs/tags/{ref}") else None
sha = git("rev-parse", ref)
previous = previous_tag(ref, tag)
print(f"release: ref={ref} sha={sha[:12]} previous={previous or '<none>'}")
notes = cross_checks(tag, sha) if tag else [
f"Rendered for `{ref}`, which is not a tag. Nothing was published."
]
for note in notes:
print(f"release: NOTE {note}")
log = commits(previous, ref)
print(f"release: {len(log)} non-merge commits in the span")
log, overflow = truncate(log)
if overflow:
print(f"release: NOTE {overflow}")
notes.append(overflow)
body = render(tag or ref, sha, previous, log, notes)
if args.dry_run or not tag:
print("--- body ---")
print(body)
return
token = os.environ.get("RELEASE_TOKEN") or os.environ.get("TOKEN")
if not token:
sys.exit("release: no RELEASE_TOKEN in the environment")
publish(tag, f"FabledCurator {tag[1:]}", body, token)
if __name__ == "__main__":
main()
+120 -146
View File
@@ -1,46 +1,23 @@
"""The two values `artifacts.sh` derives, and what each of them promises.
"""`artifacts.sh identity` is what decides whether a build gets skipped.
`revision` decides whether a build gets skipped; `version` is what an instance
reports about itself and what a release tag is named after. Neither has a
consumer that would notice it going subtly wrong.
Milestone 313 step 4: build.yml asks the registry for `<image>:<identity>` and,
on a hit, publishes NO new bytes — it repoints the channel and date tags at the
manifest already there. So the identity has to be a true name for the content.
Both ways of getting it wrong are silent at build time and only surface in
production:
## revision
* **too coarse** — two genuinely different images share an identity, so the
second one never gets built and its tags point at the first one's bytes. The
live case is FC_CHANNEL: a `dev` and a `main` build of one revision differ,
and collapsing them ships an instance that reports the wrong channel forever.
* **too fine** — the identity moves when the content did not, nothing ever
hits, and step 4 buys nothing. A commit sha would do exactly this.
Milestone 318 step 3: each image carries its revision as an `fc.revision`
label, and build.yml reads that label back off the moving channel tag. Equal
to the derived revision means the bytes this push would produce are already
published, so the build is skipped.
That makes the revision load-bearing in a way a version string is not — it is
compared for equality against a value stamped into a real published artifact.
Both ways of getting it wrong are silent:
* **it does not identify the content** — a revision that moves when the source
did not (a HEAD-derived value, say) never matches, nothing is ever skipped,
and the mechanism quietly buys nothing while every lane stays green.
* **it identifies the wrong content** — a revision that holds still when the
source DID change matches a stale label, the build is skipped, and the
channel serves bytes that do not correspond to the commit. This is the
dangerous direction, and it is what `test_artifact_paths.py` guards from the
other side by pinning the path sets.
This module owns the narrower claim: whatever the path sets say, the revision
is genuinely the commit those paths last changed in.
## version
`YYYY.MM.DD.HHMM`, zero-padded, UTC — one shape across the family (note #3127
§1, rule 148), so the string this project emits is the same string its siblings
emit. Two nearly-identical formats are more dangerous than two obviously
different ones, and the only thing keeping them identical is a test.
The identity-TAG tests this file used to hold are gone with the tag. There is
no longer a `CHANNELLED` list to drift (the channel is which tag you inspect),
and no `identity` subcommand to refuse an unqualified call.
The Dockerfiles are read here rather than trusted, because the coarse direction
appears the moment someone adds a build-arg without touching `CHANNELLED`.
"""
from __future__ import annotations
import os
import re
import subprocess
from pathlib import Path
@@ -49,118 +26,115 @@ import pytest
ROOT = Path(__file__).resolve().parent.parent
ARTIFACTS = ("web", "ml", "agent", "extension")
# Only image artifacts have an identity — the extension is cached as an
# ext-<version> Forgejo release, not a registry tag.
IMAGE_ARTIFACTS = {
"web": "Dockerfile",
"ml": "Dockerfile.ml",
"agent": "agent/Dockerfile",
}
# 12 hex chars — the prefix build.yml stamps and compares.
_REVISION = re.compile(r"^[0-9a-f]{12}$")
CHANNELS = ("main", "dev")
# YYYY.MM.DD.HHMM, every segment zero-padded to its full width.
_VERSION = re.compile(r"^\d{4}\.\d{2}\.\d{2}\.\d{4}$")
# docker's own tag grammar: [A-Za-z0-9_][A-Za-z0-9._-]{0,127}
_TAG = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$")
# `ARG FC_CHANNEL` in a Dockerfile means build.yml passes a per-channel value
# in, so the channel is part of what the image IS.
_ARG_CHANNEL = re.compile(r"^\s*ARG\s+FC_CHANNEL\b", re.MULTILINE)
# Everything here goes through artifacts.sh rather than importing a sibling
# test module. That is the interface build.yml actually calls, so the tests
# exercise the contract instead of a Python re-implementation of it — and no
# other test module in this repo imports another, so a cross-test import would
# be a new convention introduced for no gain.
def artifacts(*args: str) -> str:
return subprocess.run(
["sh", str(ROOT / "scripts" / "artifacts.sh"), *args],
def identity(artifact: str, channel: str | None = None) -> subprocess.CompletedProcess:
cmd = ["sh", str(ROOT / "scripts" / "artifacts.sh"), "identity", artifact]
if channel is not None:
cmd.append(channel)
return subprocess.run(cmd, capture_output=True, text=True, cwd=ROOT)
def ok(artifact: str, channel: str | None = None) -> str:
proc = identity(artifact, channel)
assert proc.returncode == 0, f"identity {artifact} {channel}: {proc.stderr}"
return proc.stdout.strip()
def bakes_the_channel(artifact: str) -> bool:
return bool(_ARG_CHANNEL.search((ROOT / IMAGE_ARTIFACTS[artifact]).read_text()))
@pytest.mark.parametrize("artifact", sorted(IMAGE_ARTIFACTS))
def test_channel_dependence_matches_the_dockerfile(artifact):
"""The coarse direction, caught at its source.
Whether the channel belongs in the identity is not a preference — it is
dictated by whether the Dockerfile takes it as a build-arg. Adding an
`ARG FC_CHANNEL` to another image without adding it to `CHANNELLED` would
make its dev and main builds collide, and nothing else would notice.
"""
per_channel = {c: ok(artifact, c) for c in CHANNELS}
differs = len(set(per_channel.values())) > 1
if bakes_the_channel(artifact):
assert differs, (
f"{IMAGE_ARTIFACTS[artifact]} declares ARG FC_CHANNEL, so a dev "
f"build and a main build of one revision are different images — "
f"but both derive the identity {per_channel['main']!r}. The main "
f"build would reuse the dev image and report the wrong channel. "
f"Add {artifact!r} to CHANNELLED in scripts/artifacts.sh."
)
else:
assert not differs, (
f"{IMAGE_ARTIFACTS[artifact]} takes no channel build-arg, so one "
f"revision is one image and a merge to main should reuse what dev "
f"already built — but the identity differs per channel "
f"({per_channel}), so every merge rebuilds it for nothing. Remove "
f"{artifact!r} from CHANNELLED in scripts/artifacts.sh."
)
@pytest.mark.parametrize("artifact", sorted(IMAGE_ARTIFACTS))
def test_identity_tracks_the_artifacts_own_revision(artifact):
"""The fine direction: the identity must be the revision, not the push.
`revision` is the commit this artifact's shipped files last changed in, so
it holds still across pushes that did not touch it. Anything derived from
HEAD instead would move every push and never hit the registry.
"""
rev = subprocess.run(
["sh", str(ROOT / "scripts" / "artifacts.sh"), "revision", artifact],
capture_output=True, text=True, check=True, cwd=ROOT,
).stdout
def revision(artifact: str) -> str:
return artifacts("revision", artifact).strip()
def newest_by_commit_time(artifact: str) -> str:
"""The full SHA of the newest commit touching this artifact's shipped set.
Ordered by committer TIME, matching what artifacts.sh means. Deliberately
not `git log -1`: git's default order is reverse-chronological only within
topological constraints, so on a merged history it can name a different
commit than the newest timestamp does. They agree on this repo today, and
a test that silently depends on them continuing to agree would be a flake
waiting for the branch shape that separates them.
"""
paths = artifacts("paths", artifact).split()
log = subprocess.run(
["git", "log", "--format=%ct %H", "HEAD", "--", *paths],
capture_output=True, text=True, check=True, cwd=ROOT,
).stdout.split("\n")
commits = [line.split(" ", 1) for line in log if line.strip()]
assert commits, (
f"no commit in this history touches the {artifact} path set — the "
f"derivation has nothing to stand on"
)
return max(commits, key=lambda c: int(c[0]))[1]
@pytest.mark.parametrize("artifact", ARTIFACTS)
def test_revision_is_the_commit_its_own_shipped_files_last_changed_in(artifact):
"""The claim the whole skip decision rests on.
Computed from git rather than asked of the script, so it fails if the
derivation ever stops meaning what it says — switching to HEAD, to a build
clock, or to a path set it did not actually use. Each of those still
produces a plausible 12-hex value, which is why this is worth asserting
rather than eyeballing.
"""
expected = newest_by_commit_time(artifact)
got = revision(artifact)
assert expected.startswith(got), (
f"{artifact} derives {got!r}, but the newest commit touching its "
f"shipped files is {expected[:12]!r}. The label stamped into the image "
f"would not identify its own content."
)
@pytest.mark.parametrize("artifact", ARTIFACTS)
def test_revision_is_a_legal_label_value_and_is_stable(artifact):
"""It is stamped as a docker label and compared for string equality, so a
stray newline or a varying value breaks the comparison rather than the
build — the mechanism would simply stop hitting, silently."""
first = revision(artifact)
assert _REVISION.match(first), f"{first!r} is not a 12-char hex revision"
assert first == revision(artifact), "revision is not stable across calls"
@pytest.mark.parametrize("artifact", ARTIFACTS)
def test_version_is_zero_padded_calver(artifact):
"""The family shape, pinned.
Padding was stripped until 2026-08-28 on the reasoning that each segment
should read as a plain integer — which never held, since comparison strips
leading zeros on parse anyway. What it did do was make this project emit
`2026.8.28.1432` while a sibling emitted `2026.08.28.1432`: two shapes one
character apart, which is the hard kind of difference to notice.
Also catches the midnight case. A `%H%M` of `0322` must survive as `0322`;
the old strip-leading-zeros helper turned it into `322`, silently changing
a four-digit field into three.
"""
value = artifacts("version", artifact).strip()
assert _VERSION.match(value), (
f"{artifact} derives {value!r}, which is not zero-padded "
f"YYYY.MM.DD.HHMM. Note #3127 §1 and rule 148 both specify the padded "
f"form, and a release tag is this string with a `v` in front."
)
@pytest.mark.parametrize("artifact", ARTIFACTS)
def test_version_and_revision_describe_the_same_commit(artifact):
"""They are derived independently and must not be able to disagree.
A build reports the version and skips on the revision, so a divergence
would mean an instance naming one commit while carrying another's bytes —
unfalsifiable from outside, since both values look perfectly well-formed.
"""
sha = newest_by_commit_time(artifact)
stamped = subprocess.run(
["git", "show", "-s", "--format=%cd", "--date=format-local:%Y.%m.%d.%H%M", sha],
capture_output=True, text=True, check=True, cwd=ROOT,
env={"TZ": "UTC", "PATH": os.environ.get("PATH", "")},
).stdout.strip()
assert artifacts("version", artifact).strip() == stamped
assert sha.startswith(revision(artifact))
value = ok(artifact, "main")
assert rev and rev in value, (
f"identity {value!r} does not contain the {artifact} revision {rev!r}"
)
@pytest.mark.parametrize("artifact", sorted(IMAGE_ARTIFACTS))
def test_identity_is_a_legal_docker_tag(artifact):
"""It is pushed as a tag, so an illegal one fails at the registry — after
the build has already run."""
for channel in CHANNELS:
value = ok(artifact, channel)
assert _TAG.match(value), f"{value!r} is not a valid docker tag"
def test_a_channelled_artifact_refuses_an_unqualified_identity():
"""Refusing beats defaulting. If `identity web` quietly returned the
unqualified `r-<rev>`, a workflow that forgot to pass the channel would
publish one image under a name both channels then reuse — the exact
collision the CHANNELLED list exists to prevent, reintroduced by an
omission rather than by an edit."""
proc = identity("web")
assert proc.returncode != 0, (
"identity web returned a value with no channel: "
f"{proc.stdout.strip()!r}"
)
def test_the_extension_has_no_image_identity():
"""It is cached as an ext-<version> release asset, and its cache key is the
version. Answering with a plausible image tag would invite a second,
divergent cache."""
proc = identity("extension", "main")
assert proc.returncode != 0
assert "ext-" in proc.stderr
+13 -44
View File
@@ -13,10 +13,6 @@ being right, and both ways of being wrong are silent:
Nothing else notices either. The version still derives, CI still goes green,
and the mismatch only surfaces as "I pinned that build and got the wrong
bytes". So the Dockerfiles are read here and compared against the declaration.
The COPY list is not the whole answer, though. A file that DECIDES what an
artifact reports belongs in its set even though it is copied into nothing —
see DERIVERS below, where the same finding is recorded twice (#3156, #3202).
"""
from __future__ import annotations
@@ -114,48 +110,26 @@ def test_the_web_image_versions_on_an_extension_change():
)
# A file that DECIDES an artifact's identity is part of what that artifact is
# built from, even though it is copied into no image. Both entries here are the
# same finding twice — #3156 for packaging.sh, #3202 for artifacts.sh — and
# both were latent for the same reason: the version has no backstop.
#
# The revision does. Change how a REVISION is computed and the derived value
# stops matching the label on the published image, which forces a rebuild; the
# mechanism self-corrects because it compares against a string stamped into a
# real artifact. Nothing compares a version to anything, so a version-only
# derivation change is invisible unless the deriver is in the set.
DERIVERS = [
# packaging.sh decides the version build.yml stamps into the packaged
# manifest.json, so changing it changes the shipped bytes. Left out,
# milestone 313 step 4 turns silent: the new version misses the
# ext-<version> cache and gets signed, while web's revision has not moved,
# so the reuse path republishes the old image and the fresh signature is
# orphaned. Guarded for web too, since web bundles what the extension makes.
("extension/scripts/packaging.sh", ("extension", "web")),
# artifacts.sh decides the FC_VERSION baked into the web image (#3202).
# Web only, and deliberately: ml and agent ask this script for `revision`
# alone, so they are covered by the self-correcting path above, and the
# extension takes its version from packaging.sh. Milestone 318 step 5 is
# the worked instance — b3989d0 and 5771fd5 share revision fb2c4d5b80be
# while the version moved 2026.8.28.1249 -> 2026.08.28.1249. It was
# harmless only because FC_VERSION did not exist until one commit later.
("scripts/artifacts.sh", ("web",)),
]
def test_the_web_image_versions_on_a_version_derivation_change():
"""packaging.sh ships in no image, yet it belongs in the sets that bundle
the XPI — because it decides the version string build.yml stamps into the
packaged manifest.json. Changing the derivation changes the shipped bytes.
@pytest.mark.parametrize("path, artifacts", DERIVERS, ids=lambda v: str(v))
def test_a_version_deriver_is_in_the_set_of_what_it_decides(path, artifacts):
for artifact in artifacts:
Left out, milestone 313 step 4 turns it silent: the new version misses the
ext-<version> cache and gets signed, while web's revision has not moved, so
the reuse path republishes the old image and the fresh signature is
orphaned. Guarded for web and the extension both, since web bundles what
the extension produces.
"""
for artifact in ("extension", "web"):
inc = includes(artifact)
excluded = [
p[len(":(exclude)"):] for p in declared_paths(artifact)
if p.startswith(":(exclude)")
]
path = "extension/scripts/packaging.sh"
assert any(covered_by(path, i) for i in inc), (
f"{path} decides the version {artifact} reports, but is not in the "
f"{artifact} path set. A change to the derivation would leave the "
f"revision untouched, the build skipped, and the published image "
f"reporting the old version — with nothing to disagree with it."
f"{path} is not in the {artifact} path set"
)
assert not any(
covered_by(path, e.rstrip("*").rstrip("/")) for e in excluded
@@ -172,11 +146,6 @@ def test_a_version_deriver_is_in_the_set_of_what_it_decides(path, artifacts):
# Deliberate exclusions — the too-wide direction. Each of these lives
# beside shipped code but never reaches an image, and including it
# would re-version the artifact for a change it does not carry.
#
# "Never reaches an image" is the test, not "is not source": DERIVERS
# above are also copied into nothing and DO belong in their sets,
# because they decide what the image reports. The line between the two
# lists is whether the file has a say in the artifact's identity.
("agent", "agent/README.md"),
("agent", "agent/ruff.toml"),
("agent", "agent/docker-compose.yml"),
-60
View File
@@ -9,63 +9,3 @@ async def test_health_returns_ok(client):
assert response.status_code == 200
body = await response.get_json()
assert body == {"status": "ok"}
# --- build identity (milestone 318 step 6) --------------------------------
#
# With no version image tags left, /api/health is the only place an instance
# says which build it is. Both fields are OMITTED when unset rather than sent
# empty: absence already means "cannot say" — an image predating the field
# says exactly that by not having the key — and a second spelling would make
# every reader special-case it (note #3127 §7).
#
# The test above is load-bearing for that: it asserts the body is EXACTLY
# {"status": "ok"} when nothing is stamped, so a well-meaning `or ""` default
# fails it.
@pytest.mark.asyncio
async def test_health_reports_the_build_it_is(client, monkeypatch):
from backend.app.api import health
monkeypatch.setattr(health, "FC_VERSION", "2026.08.28.1249")
monkeypatch.setattr(health, "FC_CHANNEL", "dev")
body = await (await client.get("/api/health")).get_json()
assert body == {
"status": "ok",
"version": "2026.08.28.1249",
"channel": "dev",
}
@pytest.mark.asyncio
async def test_health_keeps_the_channel_out_of_the_version(client, monkeypatch):
"""Rule 149, asserted rather than assumed.
The tempting shortcut is a `-dev` suffix on the version. The extension's
comparator parses each dotted segment with `parseInt`, so a suffixed
segment reads as 0 and every dev build compares equal to every other —
#2993 exactly. Two separate keys cannot express that mistake.
"""
from backend.app.api import health
monkeypatch.setattr(health, "FC_VERSION", "2026.08.28.1249")
monkeypatch.setattr(health, "FC_CHANNEL", "dev")
body = await (await client.get("/api/health")).get_json()
assert body["version"] == "2026.08.28.1249"
assert "dev" not in body["version"]
@pytest.mark.asyncio
async def test_health_omits_a_channel_it_cannot_name(client, monkeypatch):
"""A locally-built image has a version but no channel. It must not gain an
empty one — the key's absence is the answer."""
from backend.app.api import health
monkeypatch.setattr(health, "FC_VERSION", "2026.08.28.1249")
monkeypatch.setattr(health, "FC_CHANNEL", "")
body = await (await client.get("/api/health")).get_json()
assert body == {"status": "ok", "version": "2026.08.28.1249"}
-149
View File
@@ -1,149 +0,0 @@
"""What the release changelog promises, and the way it would lie quietly.
A changelog has no consumer that checks it. If it lists the wrong span nothing
fails — the release publishes, reads perfectly, and tells the operator that a
month of work landed in a build that never contained it. That is the same
silent-and-plausible failure class as a revision that identifies the wrong
content (`test_artifact_identity.py` guards the other side of it), so the span
selection is asserted rather than eyeballed.
Everything runs the script the way `release.yml` runs it — as a subprocess,
through `--dry-run`. That is the same code path as a real publish right up to
the HTTP call, so these exercise the interface CI uses instead of a Python
re-implementation of it.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
SCRIPT = ROOT / "scripts" / "release_notes.py"
def notes(*args: str, cwd: Path | None = None) -> str:
return subprocess.run(
["python3", str(SCRIPT), "--dry-run", *args],
capture_output=True, text=True, check=True, cwd=cwd or ROOT,
).stdout
def body_of(out: str) -> str:
assert "--- body ---" in out, f"no body was rendered:\n{out}"
return out.split("--- body ---", 1)[1]
def git(repo: Path, *args: str) -> str:
return subprocess.run(
["git", "-c", "user.email=ci@example.invalid", "-c", "user.name=ci",
"-c", "commit.gpgsign=false", *args],
capture_output=True, text=True, check=True, cwd=repo,
).stdout.strip()
@pytest.fixture
def shaped_history(tmp_path: Path) -> Path:
"""Three releases spanning the rule 148 tag-shape change.
Ancestry order is `v26.06.04.0` → `v2026.08.28.2208` → `v2026.08.29.1000`,
which is the exact arrangement where walking ancestry and sorting a list
disagree — see the test below. Synthetic rather than taken from this repo's
own tags so it holds whether or not CI's checkout brought the tags along:
a span test that quietly skips is the one outcome worse than a failing one.
"""
repo = tmp_path / "shaped"
repo.mkdir()
git(repo, "init", "-q", "-b", "main")
for i, tag in enumerate(("v26.06.04.0", "v2026.08.28.2208", "v2026.08.29.1000")):
(repo / "f.txt").write_text(f"{i}\n")
git(repo, "add", "f.txt")
git(repo, "commit", "-q", "-m", f"work landing in {tag}")
git(repo, "tag", tag)
# One more commit and a merge, so the merge-exclusion test has something to
# exclude that a first-parent listing would otherwise show.
git(repo, "checkout", "-q", "-b", "side")
(repo / "g.txt").write_text("side\n")
git(repo, "add", "g.txt")
git(repo, "commit", "-q", "-m", "feat: work done on the side branch")
git(repo, "checkout", "-q", "main")
git(repo, "merge", "-q", "--no-ff", "side", "-m", "Merge pull request #999 from side")
git(repo, "tag", "v2026.08.30.0900")
return repo
def test_the_previous_release_is_found_by_ancestry_not_by_sorting(shaped_history):
"""The trap this repo is standing in right now.
Rule 148 moved the tag shape from `v26.05.22.0` to `v2026.08.28.2208`.
Lexicographically `v2026...` sorts BEFORE `v26...` — the third character is
`0` against `6` — so a sorted-list implementation reaches back past every
new-shape tag to the newest OLD-shape one and emits months of commits as
"changes since". It looks entirely correct on any repo whose tags share a
single shape, which is every repo until the day the shape changes.
Here, ancestry says `v2026.08.28.2208` and sorting says `v26.06.04.0`.
"""
out = notes("v2026.08.29.1000", cwd=shaped_history)
assert "previous=v2026.08.28.2208" in out
assert "v26.06.04.0" not in out
def test_the_body_names_the_span_it_actually_listed(shaped_history):
"""A body whose heading says "since X" over commits computed from Y is
unfalsifiable from outside — both halves read fine on their own."""
body = body_of(notes("v2026.08.29.1000", cwd=shaped_history))
assert "## Changes since v2026.08.28.2208" in body
assert "v2026.08.28.2208..v2026.08.29.1000" in body
assert "work landing in v2026.08.29.1000" in body
assert "work landing in v2026.08.28.2208" not in body
def test_merges_are_excluded_so_the_list_is_the_work(shaped_history):
"""Rule 153 merges dev into main with a plain merge commit, so `main`'s
first-parent view is nothing but "Merge pull request #N". Including those
would publish a changelog of PR numbers over the actual changes."""
body = body_of(notes("v2026.08.30.0900", cwd=shaped_history))
assert "feat: work done on the side branch" in body
assert "Merge pull request #999" not in body
def test_the_first_release_still_renders_with_nothing_behind_it(shaped_history):
"""No previous tag is reachable from the oldest one. That is a real state,
not an error, and it must not take the release down with it."""
out = notes("v26.06.04.0", cwd=shaped_history)
assert "previous=<none>" in out
assert "## Changes" in body_of(out)
def test_a_non_tag_ref_renders_but_refuses_to_claim_it_published():
"""`--dry-run HEAD` is the operator's preview before deciding to cut a tag
at all. It must not describe itself as a release that happened."""
out = notes("HEAD")
assert "which is not a tag" in out
body_of(out)
def test_the_rollback_refs_name_all_three_images():
"""Rule 145: `:c-<sha>` is the rollback unit, and the three images move
together. A release listing only the web image sends an operator into a
rollback that leaves ml and agent on the newer build — the exact mismatch
build.yml builds all three on every push to avoid."""
body = body_of(notes("HEAD"))
for image in ("fabledcurator", "fabledcurator-ml", "fabledcurator-agent"):
assert f"bvandeusen/{image}:c-" in body, f"{image} missing from the rollback refs"
def test_an_unbounded_span_is_truncated_and_says_so():
"""With no reachable previous tag the span is the whole history. Emitting
eleven hundred lines would bury the one line explaining why there are
eleven hundred of them, so the cap is part of the message, not a silent
slice."""
out = notes("HEAD")
if "previous=<none>" not in out:
pytest.skip("a previous tag is reachable from HEAD in this checkout")
body = body_of(out)
listed = [ln for ln in body.split("\n") if ln.startswith("- ")]
assert len(listed) <= 200
assert "more than a changelog is for" in body