Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
aef97d6
refactor!: replace configs gem with registry index
gildesmarais Aug 22, 2026
f103414
feat(registry): sync CLI, seed bundle, and background refresh
gildesmarais Aug 22, 2026
5725ba0
docs: registry sync runbook and catalog API ownership
gildesmarais Aug 22, 2026
a440551
docs: add registry go-live manual
gildesmarais Aug 22, 2026
22da0da
fix(deps): pin html2rss from GitHub for CI
gildesmarais Aug 22, 2026
c61e567
feat(registry): sync governance with staged promote and trust context
gildesmarais Aug 22, 2026
4ba7386
chore(deps): pin html2rss to registry hardening commit
gildesmarais Aug 22, 2026
8ff3bfa
fix(ci): use SimpleCov add_filter and git html2rss pin
gildesmarais Aug 22, 2026
45dc4f5
refactor(registry): fold sync satellites into sync_transport
gildesmarais Aug 22, 2026
0cfeb9c
refactor(registry): fold catalog wire and load guards into index
gildesmarais Aug 22, 2026
043fc7e
refactor(registry): centralize manifest I/O and dedup test signing
gildesmarais Aug 22, 2026
076964e
chore(registry): dedup fixtures and sync docs
gildesmarais Aug 22, 2026
98d6ccb
refactor(ruby4): apply leading operators and it blocks on registry br…
gildesmarais Aug 22, 2026
d82231e
refactor(registry): dedupe transport, status lookup, and deep_dup
gildesmarais Aug 22, 2026
a0810a0
perf(registry): use Set for catalog diff and tighten hot paths
gildesmarais Aug 22, 2026
60adb24
test(registry): drop send pins and table-drive transport specs
gildesmarais Aug 22, 2026
f6756f7
refactor(api): modernize configs and root_metadata helpers
gildesmarais Aug 22, 2026
1261dcf
fix(registry): restore rubocop disables and constant-cache memoization
gildesmarais Aug 22, 2026
c9c8dec
docs: codify Ruby 4.0 style for agents and contributors
gildesmarais Aug 22, 2026
77c9abd
refactor(registry): modernize registry subsystem, unify types, and do…
gildesmarais Aug 23, 2026
468dc5d
refactor(registry): streamline registry modules, unify StructuredData…
gildesmarais Aug 23, 2026
63066d9
refactor(registry): map catalog wire fields at Index edge
gildesmarais Aug 23, 2026
0830b30
chore(deps): pin html2rss Gemfile.lock to registry-v1 tip
gildesmarais Aug 23, 2026
9fcb521
fix(registry): implement cli_exit_code, atomic store swap, index fres…
gildesmarais Aug 24, 2026
f30d1d6
feat(cli): add unified operator CLI and embed 144 curated seed configs
gildesmarais Aug 24, 2026
8c1dc70
docs(compose): add registry-sync service and compose operator workflow
gildesmarais Aug 24, 2026
a8950b6
chore(compose): remove legacy watchtower service
gildesmarais Aug 24, 2026
63c06d0
refactor(registry): implement embedded bundle lookup and eliminate se…
gildesmarais Aug 24, 2026
2a1e9e5
feat(docker): bake official registry artifact at build time and drop …
gildesmarais Aug 24, 2026
b36b10c
docs(registry): align terminology and document embedded registry deli…
gildesmarais Aug 24, 2026
25ff6a1
feat(docker): verify official registry signature and digests at build…
gildesmarais Aug 24, 2026
14d0315
feat(cli): add registry verify subcommand and invoke in Docker build
gildesmarais Aug 24, 2026
7f2e301
chore(bundle): update html2rss git revision in Gemfile.lock
gildesmarais Aug 24, 2026
0afb12f
refactor(config)!: drop feeds.yml and take instance policy from the e…
gildesmarais Oct 15, 2026
26fe073
feat(cli): add a local registry bundle with registry add
gildesmarais Oct 15, 2026
b7b46bc
chore(deps): pin html2rss to the single-feed YAML loader
gildesmarais Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ frontend/.astro

# Bundler/vendored dependencies
/vendor/

# Local bundle builds
/dist/

5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins:
- rubocop-thread_safety

AllCops:
TargetRubyVersion: 4.0
DisplayCopNames: true
NewCops: enable
Exclude:
Expand Down Expand Up @@ -38,6 +39,10 @@ Style/Documentation:
AllowedConstants:
- App

Style/ItBlockParameter:
Enabled: true
EnforcedStyle: allow_single_line

RSpec/SpecFilePathFormat:
Exclude:
- 'spec/html2rss/web/app/*_spec.rb'
Expand Down
50 changes: 46 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,63 @@ See [docs/design-system.md](docs/design-system.md) for visual rules.
- **No host execution:** All commands MUST run inside the Dev Container via `make` or `bundle exec`.
- **No skipped quality gate:** Opening a PR without a green Dev Container gate is forbidden. If the gate cannot run, do not open the PR; fix the environment or hand off with explicit blocker + next command for the user.

## Ruby 4 Style

**Ruby 4.0+ only** (see `.tool-versions`). No Ruby 3.x backward-compat shims, guards, or dual-path APIs.

### Baseline

- `# frozen_string_literal: true` on every `.rb` file
- Plain Ruby — no ActiveSupport
- Keyword arguments for public multi-arg APIs
- Typed YARD on public methods in `app/` (`@param`, `@return`) — enforced by `make yard-verify-public-docs`

### Modern syntax (prefer consistently)

| Idiom | Use instead of |
| --- | --- |
| Leading `&&` / `\|\|` at line start (Ruby 4) | Trailing operators on long wrapped conditions |
| `it` in single-parameter blocks | `{ \|x\| x.foo }` when block has one arg only |
| Pattern matching (`in`, `case … in`) | Deep `if/elsif` chains on shape |
| `Data.define` | OpenStruct / hand-rolled structs |
| `filter_map`, `index_by`, `then`, `match?` | Verbose `map`/`compact`, nested `if`, `=~` |
| Endless `def` | One-line pure helpers when RuboCop allows |
| Core `Set` (no `require 'set'`) | Array membership/diff on growing collections |

### Performance (agent defaults)

- **Set** for catalog/diff/membership when sizes can grow
- **Memoize** repeated `ENV.fetch` / pure computations on hot paths
- **One owner** for duplicated helpers — dedupe before splitting into new files
- **Functional iterators** over imperative loops
- **No metric-driven micro-methods** whose only purpose is satisfying RuboCop metrics
- Do **not** document ZJIT/Ruby Box/Ractor as defaults

### Web-specific deltas

- Prefer `class << self` + `private` over `module_function` (see docs/README Architectural Constraints)
- Do not use `send(...)` to reach private APIs in app code or specs
- Specs: table-drive matrices; `:aggregate_failures` for discriminating multi-assert examples
- LOC: dedupe/unify before extracting — new files only when they buy a real seam or test surface

## Config catalog API

Public feed-directory metadata for embedded and local configs.
Public feed-directory metadata from verified registry bundles (including the built-in `local` path bundle when `catalog: true`).

| Item | Detail |
| --- | --- |
| Endpoint | `GET /api/v1/configs` |
| Flag | `CONFIG_CATALOG_ENABLED` (default `true`; set `false` to disable) |
| Disabled response | `404` with `{ "error": "catalog_disabled" }` |
| Embedded entries | `Html2rss::Configs::Catalog.entries` — do not re-walk YAML in the handler |
| Local entries | `Catalog::Merge` includes `feeds.yml` feeds only when `directory.title` is set |
| Registry entries | `Registry::Index.current.catalog_rows` — loads signed/path bundles from `config/registries.yml`; adds `source: registry`, `registry: <id>` |
| Local registry | Built-in `local` at `data/registries/local` (`catalog: false` by default); populate with `html2rss-web registry add FILE_OR_DIR` |
| Per-registry privacy | `catalog: false` in `registries.yml` omits that registry from the API (feeds still served) |
| Starter feeds (UI) | Frontend `selectStarterFeeds` when feed creation is disabled; catalog find uses full catalog when enabled |
| Catalog find | `findCatalogEntries` → multi-hit list under create URL; links via `catalogFeedHref` (path + defaults) |
| CORS | Route-scoped on `/api/v1/configs` only (`GET`, `OPTIONS`) |
| Root metadata | `GET /api/v1/` exposes `instance.catalog: { enabled, url }` |
| Root metadata | `GET /api/v1/` exposes `instance.catalog: { enabled, url }` and `instance.registries` sync status |
| Contract SSOT | Request specs under `spec/html2rss/web/api/v1_spec.rb` and generated `public/openapi.yaml` |

Registry sync: `bin/html2rss-web registry status`; embedded bundle + optional runtime sync via `Registry::Sync.boot!`. See [docs/README.md](docs/README.md#registry-sync-runbook).

After handler or envelope changes: `make openapi` and `make ci-ready`.
9 changes: 9 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,12 @@ Audit channel: snake_case `security_event` with IP / user-agent / token hash. Au

### LogEvent
Shared emit plumbing for both channels (`RequestContext`, `LogSanitizer`, `AppLogger` / Sentry). Not a third public facade.

### Registry Index
Sole feed repository over registry bundles (official sync/path plus the built-in `local` path bundle). Builds catalog wire rows, enforces load-time trust and channel-domain allowlists, and serves `config_for` / `catalog_rows` / `status`.

### Registry Sync
Backend orchestration for fetch → verify → stage/promote of signed registry bundles. Owns boot initialization, background refresh, CLI exit codes, and catalog-change telemetry after promotion.

### Sync Transport
Backend HTTPS fetch, sync URL resolution (GitHub releases and channel defaults), and manifest version gating used by `Registry::Sync` and parse-time config resolution.
28 changes: 25 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,26 @@ RUN apk add --no-cache \
/usr/local/bundle/bundler/gems/*/.git \
/usr/local/bundle/cache/bundler/git

# Stage 3: Runtime
# Stage 3: Official Registry Artifact Builder & Verifier
FROM ${RUBY_BASE_IMAGE} AS registry-builder

ARG REGISTRY_BUNDLE_URL="https://github.com/html2rss/html2rss-configs/releases/latest/download/registry-bundle.tar.gz"

WORKDIR /app

COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY bin ./bin
COPY app ./app
COPY config ./config

# hadolint ignore=DL3018
RUN apk add --no-cache curl tar ca-certificates \
&& curl -fsSL -o registry-bundle.tar.gz "${REGISTRY_BUNDLE_URL}" \
&& mkdir -p /build/official \
&& tar -xzf registry-bundle.tar.gz -C /build/official \
&& bin/html2rss-web registry verify --registry official --dir /build/official

# Stage 4: Runtime
FROM ${RUBY_BASE_IMAGE}

LABEL maintainer="Gil Desmarais <html2rss-web-docker@desmarais.de>"
Expand All @@ -51,13 +70,14 @@ ARG GIT_SHA
ENV PORT=4000 \
RACK_ENV=production \
RUBY_YJIT_ENABLE=1 \
PATH="/app/bin:$PATH" \
BUILD_TAG=${BUILD_TAG} \
GIT_SHA=${GIT_SHA}

EXPOSE $PORT

HEALTHCHECK --interval=30m --timeout=60s --start-period=5s \
CMD ["/app/bin/docker-healthcheck"]
CMD ["/app/bin/html2rss-web", "healthcheck"]

ARG USER=html2rss
ARG UID=991
Expand All @@ -78,17 +98,19 @@ RUN apk add --no-cache \
&& mkdir -p /app \
&& mkdir -p /app/tmp/rack-cache-body \
&& mkdir -p /app/tmp/rack-cache-meta \
&& mkdir -p /app/data/registries \
&& chown "$USER":"$USER" -R /app

WORKDIR /app

USER 991

COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --chown=$USER:$USER bin/docker-healthcheck ./bin/docker-healthcheck
COPY --chown=$USER:$USER bin ./bin
COPY --chown=$USER:$USER Gemfile Gemfile.lock app.rb config.ru ./
COPY --chown=$USER:$USER app ./app
COPY --chown=$USER:$USER config ./config
COPY --from=registry-builder --chown=$USER:$USER /build/official ./registries/official
COPY --chown=$USER:$USER public ./public
COPY --from=frontend-builder --chown=$USER:$USER /app/frontend/dist ./frontend/dist

Expand Down
9 changes: 2 additions & 7 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@ source 'https://rubygems.org'

git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

gem 'html2rss', '~> 0.27'
# gem 'html2rss', github: 'html2rss/html2rss', branch: 'master'
gem 'html2rss-configs', github: 'html2rss/html2rss-configs'

# Use these instead of the two above (uncomment them) when developing locally:
# gem 'html2rss', path: '../html2rss'
# gem 'html2rss-configs', path: '../html2rss-configs'
# Until rubygems 0.28.0: git branch; local monorepo: BUNDLE_LOCAL__HTML2RSS=/path/to/html2rss
gem 'html2rss', github: 'html2rss/html2rss', branch: 'feat/registry-v1'

gem 'base64'
gem 'rack-cache'
Expand Down
Loading