Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions .agents/skills/html2rss-config/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: html2rss-config
description: >-
Create or repair curated html2rss YAML feed configs in this repo (lib/html2rss/configs/),
Create or repair curated html2rss YAML feed configs in this repo (configs/),
including directory.topics, directory.title, selectors, Faraday vs Botasaurus triage, RSS probe-before-write,
and the AGENTS.md quality gate. Use when adding a new feed config, fixing a broken/zero-item
config, tightening selectors, diagnosing fetch failures, or shipping a user-requested batch
Expand All @@ -17,7 +17,7 @@ Thin workflow skill for **one config quality loop at a time** (a multi-config PR

| Mode | When | Reference |
| -------- | -------------------------------------------------------- | ------------------------------------------ |
| `new` | Add a YAML under `lib/html2rss/configs/<domain>/` | [reference/new.md](reference/new.md) |
| `new` | Add a YAML under `configs/<domain>/` | [reference/new.md](reference/new.md) |
| `repair` | Fix existing config (zero items, fetch fail, noisy feed) | [reference/repair.md](reference/repair.md) |

Pick mode from the user ask. Grow later with more modes/references; keep this file short.
Expand Down Expand Up @@ -55,7 +55,7 @@ Run from repo root. Prefer these over ad‑hoc CLI glue:
| [`scripts/analyze_html`](scripts/analyze_html) | Selector hints from cached HTML / ledger (`--from-ledger`). No network. |
| [`scripts/probe_rss`](scripts/probe_rss) | First-party RSS probe (HTML `rel=alternate` then path guesses). Exit `0` = none; `3` = found (consider drop). Under `set -e`, check `$?` — do not treat `3` as failure. |
| [`scripts/check_config`](scripts/check_config) | `validate` + `feed` (fail on 0 items); optional `--fetch` / `--botasaurus`. Resolves CLI via PATH or sibling `../html2rss`. |
| [`scripts/register_botasaurus`](scripts/register_botasaurus) | Idempotent sorted add to `spec/support/botasaurus_fetch_configs.rb`. |
| [`scripts/register_botasaurus`](scripts/register_botasaurus) | Idempotent sorted add to `test/support/botasaurus_fetch_configs.rb`. |

Examples:

Expand All @@ -76,7 +76,7 @@ From AGENTS.md Quality Gate, in order:
2. `make validate` (this repo) when touching shared support files or multiple configs
3. `make test` (non-fetch)
4. Focused fetch via `scripts/check_config … --fetch` or:
- Faraday: `bundle exec rspec --tag fetch --example 'domain/file.yml' spec/html2rss/configs_dynamic_spec.rb`
- Faraday: `bundle exec rspec --tag fetch --example 'domain/file.yml' test/configs_dynamic_spec.rb`
- Botasaurus: same with `BOTASAURUS_SCRAPER_URL=http://localhost:4010`
5. If `strategy: botasaurus` (or fetch only works via Botasaurus): `scripts/register_botasaurus domain/file.yml` — **required**.

Expand Down
6 changes: 3 additions & 3 deletions .agents/skills/html2rss-config/reference/batch.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ Dry plan (no network): `batch_recon --dry-run --file candidates.tsv`
--from-ledger tmp/html2rss-recon/ledger.tsv
```

Write YAMLs only for `BUILD` rows under `lib/html2rss/configs/<registrable-domain>/`. Use MCP/`check_config` only when cache analysis is insufficient. Botasaurus scrape: thin/empty/blocked HTML only — one retry max (`wait_timeout_seconds` ≤ 30, work budget), then drop.
Write YAMLs only for `BUILD` rows under `configs/<registrable-domain>/`. Use MCP/`check_config` only when cache analysis is insufficient. Botasaurus scrape: thin/empty/blocked HTML only — one retry max (`wait_timeout_seconds` ≤ 30, work budget), then drop.

YAML notes: [new.md](new.md). Topics: [topics.md](topics.md).

## Phase 2 — batched verification

```bash
# offline validate (example: sibling CLI)
html2rss validate lib/html2rss/configs/domain/*.yml
html2rss validate configs/domain/*.yml

# parallel feed checks (Faraday group)
.agents/skills/html2rss-config/scripts/check_config domain/a.yml &
Expand All @@ -63,7 +63,7 @@ wait
bundle exec rspec --tag fetch \
--example 'domain/a.yml' \
--example 'domain/b.yml' \
spec/html2rss/configs_dynamic_spec.rb
test/configs_dynamic_spec.rb
```

## Phase 3 — campaign gate
Expand Down
54 changes: 39 additions & 15 deletions .agents/skills/html2rss-config/reference/catalog.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
# Catalog metadata (Feed Directory)

Every YAML in this repo is a catalog entry. Embedded serialization is owned by `Html2rss::Configs::Catalog` — consumers call `Catalog.entries`; do not re-walk YAML in other repos.
Every YAML in this repo is a catalog entry. Declare feed identity explicitly — do not rely on filesystem path.

## Feed identity (`registry.id`)

```yaml
registry:
id: europa.eu/europarl/press-room
aliases: [] # optional; previous ids after a rename
directory:
title: "European Parliament — Press room"
topics: [civic]
```

| Field | Rule |
| --- | --- |
| `registry.id` | Unique within the bundle; slug `org/surface[/variant]`; lowercase; `[a-z0-9._/-]`; no `www.` prefix |
| `registry.aliases` | Optional previous ids (same bundle); resolved at feed lookup; **not** listed in catalog API |

Pick a sensible path under `configs/` (group by organization). Path is contributor ergonomics only.

**Rename workflow:** change `registry.id`; add the old id to `aliases`; remove the alias in a later release when comfortable.

## Required fields

| Field | Rule |
| ------------------ | -------------------------------------------------------------- |
| `registry.id` | Explicit feed identity (see above) |
| `directory.topics` | 1–2 values from controlled vocabulary — [topics.md](topics.md) |
| `directory.title` | Human label: `{Organization} — {Feed surface}` (em dash) |
| `channel.title` | Same string as `directory.title` (RSS and OPML) |
Expand All @@ -22,9 +43,9 @@ Every YAML in this repo is a catalog entry. Embedded serialization is owned by `

| Config path | `directory.summary` |
| ------------------------- | --------------------------------------------------------------------- |
| `who.int/news.yml` | Official news and statements from the World Health Organization. |
| `apple.com/newsroom.yml` | Product announcements and press releases from Apple. |
| `github.com/releases.yml` | Release notes for a GitHub repository (owner and name as parameters). |
| `configs/who.int/news.yml` | Official news and statements from the World Health Organization. |
| `configs/apple/newsroom.yml` | Product announcements and press releases from Apple. |
| `configs/github.com/releases.yml` | Release notes for a GitHub repository (owner and name as parameters). |

## Parameterized configs

Expand All @@ -37,24 +58,27 @@ Describe feed intent in `directory.title`, not the template URL.

| Config path | `directory.title` |
| ------------------------ | -------------------------------- |
| `anthropic.com/news.yml` | Anthropic — News |
| `who.int/news.yml` | World Health Organization — News |
| `apnews.com/hub.yml` | AP News — Top stories |
| `configs/anthropic.com/news.yml` | Anthropic — News |
| `configs/who.int/news.yml` | World Health Organization — News |
| `configs/apnews.com/hub.yml` | AP News — Top stories |

## Verification

```bash
bundle exec rspec spec/lib/html2rss/configs/catalog_spec.rb
bundle exec rspec test/registry_build_spec.rb
make validate
make registry-build
```

`Catalog.build_entry` raises `MissingDirectoryTitle` when `directory.title` is absent.
`make registry-build` emits a `registry.v1` bundle (release CI signs with `--sign`); `Html2rss::Registry::CatalogBuilder.build_entry` raises `MissingDirectoryTitle` when `directory.title` is absent.

## Downstream consumers
## Downstream pipeline

| Consumer | How it reads catalog data |
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `html2rss-web` | `GET /api/v1/configs` merges `Catalog.entries` with local `feeds.yml` entries that include `directory.title` |
| Feed Directory (docs site) | Fetches catalog JSON from a running `html2rss-web` instance |
| Layer | Owner | Responsibility |
| --- | --- | --- |
| YAML + bundle | `html2rss-configs/` | `configs/`; `tool/registry-build` → signed tarball on release |
| Catalog rows | `html2rss/` | `Html2rss::Registry::CatalogBuilder` from verified bundle manifest |
| Sync + API | `html2rss-web/` | `config/registries.yml`, `Registry::Sync`, `Registry::Index`, `GET /api/v1/configs` |
| Browse UI | `html2rss.github.io/` | Fetches catalog JSON from a running instance |

Do not duplicate catalog expansion logic outside `Html2rss::Configs::Catalog`.
Wire rows add `source: registry` and `registry: <registry_id>` in the web handler. Do not duplicate catalog expansion outside `CatalogBuilder`.
2 changes: 1 addition & 1 deletion .agents/skills/html2rss-config/reference/new.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Add one curated config. SSOT details: [AGENTS.md](../../../../AGENTS.md).
1. Pick the cleanest list URL (newsroom / archive / category — not marketing homepage). Confirm with `curl -I -L` (canonical host; no HTTPS→HTTP downgrade unless you plan Botasaurus).
2. Confirm no useful first-party RSS **for that exact URL** (else drop/defer). Use `scripts/probe_rss`. Exit `3` = feed found.
3. Capture items via skill tool order (MCP → CLI → Botasaurus → Chrome). If `auto` is empty, still inspect HTML before assuming JS-only — see [pitfalls.md](pitfalls.md).
4. Write YAML under `lib/html2rss/configs/<registrable-domain>/<name>.yml`.
4. Write YAML under `configs/<registrable-domain>/<name>.yml`.
5. Run `scripts/check_config …` (and `--fetch` / `--botasaurus` as needed); verify real `<item>` rows, not only the summary. `scripts/register_botasaurus` if Botasaurus-backed.
6. Handoff per skill.

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/html2rss-config/reference/pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Present constraints for config campaigns (N≥1). Quality-gate commands stay in
bundle exec rspec --tag fetch \
--example 'domain/a.yml' \
--example 'domain/b.yml' \
spec/html2rss/configs_dynamic_spec.rb
test/configs_dynamic_spec.rb
```

Botasaurus lane: same command with `BOTASAURUS_SCRAPER_URL=http://localhost:4010`.
Expand Down
12 changes: 6 additions & 6 deletions .agents/skills/html2rss-config/scripts/check_config
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Validate + feed (+ optional focused fetch) for one curated config.
# Usage (from repo root):
# .agents/skills/html2rss-config/scripts/check_config lib/html2rss/configs/domain/file.yml
# .agents/skills/html2rss-config/scripts/check_config configs/domain/file.yml
# .agents/skills/html2rss-config/scripts/check_config domain/file.yml --fetch
# .agents/skills/html2rss-config/scripts/check_config domain/file.yml --fetch --botasaurus

Expand All @@ -18,7 +18,7 @@ def usage!
end

def repo_root
Pathname.new(__dir__).ascend.find { |p| (p / 'lib/html2rss/configs').directory? } ||
Pathname.new(__dir__).ascend.find { |p| (p / 'configs').directory? } ||
abort('Could not find html2rss-configs repo root')
end

Expand All @@ -27,14 +27,14 @@ def resolve_config(arg, root)
candidates = []
candidates << path if path.absolute?
candidates << (root / path)
candidates << (root / 'lib/html2rss/configs' / path)
candidates << (root / 'configs' / path)
found = candidates.find(&:file?)
abort("Config not found: #{arg}") unless found
found.expand_path
end

def feed_example_name(config_path, root)
config_path.relative_path_from(root / 'lib/html2rss/configs').to_s
config_path.relative_path_from(root / 'configs').to_s
end

def invoke_entry(entry, sibling)
Expand Down Expand Up @@ -86,7 +86,7 @@ config = resolve_config(ARGV.fetch(0), root)
example = feed_example_name(config, root)
cli = resolve_html2rss_invocation(root)
cli_chdir = cli.first == 'bundle' ? (root.parent / 'html2rss').to_s : nil
env = {}
env = { 'BUNDLE_GEMFILE' => (root / 'tool/Gemfile').to_s }
env['BOTASAURUS_SCRAPER_URL'] = ENV.fetch('BOTASAURUS_SCRAPER_URL', 'http://localhost:4010') if botasaurus

Dir.chdir(root) do
Expand All @@ -101,7 +101,7 @@ Dir.chdir(root) do

if fetch
cmd = ['bundle', 'exec', 'rspec', '--tag', 'fetch', '--example', example,
'spec/html2rss/configs_dynamic_spec.rb']
'test/configs_dynamic_spec.rb']
run!(*cmd, env: env)
end
end
Expand Down
12 changes: 6 additions & 6 deletions .agents/skills/html2rss-config/scripts/register_botasaurus
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Idempotently add a config path to spec/support/botasaurus_fetch_configs.rb (sorted).
# Idempotently add a config path to test/support/botasaurus_fetch_configs.rb (sorted).
# Usage (from repo root):
# .agents/skills/html2rss-config/scripts/register_botasaurus domain/file.yml
# .agents/skills/html2rss-config/scripts/register_botasaurus lib/html2rss/configs/domain/file.yml
# .agents/skills/html2rss-config/scripts/register_botasaurus configs/domain/file.yml

require 'pathname'

Expand All @@ -16,23 +16,23 @@ def usage!
end

def repo_root
Pathname.new(__dir__).ascend.find { |p| (p / 'spec/support/botasaurus_fetch_configs.rb').file? } ||
Pathname.new(__dir__).ascend.find { |p| (p / 'test/support/botasaurus_fetch_configs.rb').file? } ||
abort('Could not find html2rss-configs repo root')
end

def normalize_name(arg, root)
rel = arg.to_s.split('lib/html2rss/configs/', 2).last.delete_prefix('/').delete_prefix('./')
rel = arg.to_s.split('configs/', 2).last.delete_prefix('/').delete_prefix('./')
abort('Expected something like domain/file.yml') unless rel.match?(%r{\A[^/]+/.+\.yml\z})

warn "Warning: missing #{root / 'lib/html2rss/configs' / rel}" unless (root / 'lib/html2rss/configs' / rel).file?
warn "Warning: missing #{root / 'configs' / rel}" unless (root / 'configs' / rel).file?
rel
end

usage! if ARGV.size != 1 || ARGV.include?('-h') || ARGV.include?('--help')

root = repo_root
name = normalize_name(ARGV.fetch(0), root)
registry = root / 'spec/support/botasaurus_fetch_configs.rb'
registry = root / 'test/support/botasaurus_fetch_configs.rb'
text = registry.read

unless text =~ /CONFIGS = %w\[(.*?)\].freeze/m
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Release

"on":
push:
tags:
- 'v*'

env:
BUNDLE_GEMFILE: tool/Gemfile

jobs:
release:
runs-on: ubuntu-latest
environment: registry-release
permissions:
contents: write

steps:
- uses: actions/checkout@v4

- name: Install dependencies
run: sudo apt-get install -yqq yamllint

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '4.0'
bundler-cache: true

- name: Run quality gate
run: make ready

- name: Build signed registry bundle
env:
REGISTRY_VERSION: ${{ github.ref_name }}
REGISTRY_SIGNING_KEY: ${{ secrets.REGISTRY_SIGNING_KEY }}
run: make registry-build -- --sign

- name: Verify release artifact
env:
REGISTRY_PUBLIC_KEY_ID: html2rss:registry:2026
REGISTRY_PUBLIC_KEY_PEM: ${{ secrets.REGISTRY_PUBLIC_KEY_PEM }}
run: |
bundle exec ruby -rhtml2rss -ropenssl -rtmpdir -e "
Dir.mktmpdir do |dir|
File.open('dist/registry-bundle.tar.gz', 'rb') do |io|
Html2rss::Registry::Archive.extract!(io, into: dir)
end
public_key = OpenSSL::PKey.read(ENV.fetch('REGISTRY_PUBLIC_KEY_PEM'))
public_keys = { ENV.fetch('REGISTRY_PUBLIC_KEY_ID') => public_key }
bundle = Html2rss::Registry::Bundle.load(dir, trust: :signed, public_keys:)
abort 'empty catalog' if bundle.catalog_entries.empty?
puts \"Verified #{bundle.catalog_entries.size} catalog entries\"
end
"

- name: Upload release artifact
uses: softprops/action-gh-release@v2
with:
files: dist/registry-bundle.tar.gz
generate_release_notes: true
draft: true
45 changes: 11 additions & 34 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,41 @@ name: Ruby
pull_request:
branches: [master]

env:
BUNDLE_GEMFILE: tool/Gemfile

jobs:
lint:
ready:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install dependencies
run: sudo apt-get install -yqq yamllint

- name: Install Node dependencies
run: npm ci --ignore-scripts --no-fund --no-audit

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
ruby-version: '4.0'
bundler-cache: true

- name: Run linters
run: make lint

- name: Validate configs
run: make validate

test:
strategy:
fail-fast: false
matrix:
ruby: ["3.3"]

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true

- name: Run tests
run: make test
- name: Run quality gate
run: make ready

test_changed_configs:
runs-on: ubuntu-latest

env:
BUNDLE_GEMFILE: tool/Gemfile

steps:
- uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
ruby-version: '4.0'
bundler-cache: true

- name: setup remote origin
Expand Down
Loading
Loading