Skip to content

Fedora RPM builds, flaky test fix, dependency advisories, and a docs correctness pass - #140

Open
samueltuyizere wants to merge 5 commits into
mainfrom
feat/fedora-builds-and-maintenance
Open

Fedora RPM builds, flaky test fix, dependency advisories, and a docs correctness pass#140
samueltuyizere wants to merge 5 commits into
mainfrom
feat/fedora-builds-and-maintenance

Conversation

@samueltuyizere

Copy link
Copy Markdown
Collaborator

Four pieces of maintenance plus one new deliverable. Three commits, each self-contained.

Fix the flaky watcher test

TestWatchConfig_DetectsFileChange failed roughly 1 in 4 full-suite race runs and blocked pushes through the pre-push hook. Two races were involved:

  • WatchConfig registers its fsnotify watch asynchronously, so the test's sleep(200ms)-then-write could lose the write entirely.
  • AtomicConfig.Reload runs reload callbacks before publishing the new pointer, so asserting via Get() could read the old config. This is the one that actually reproduced.

Adds WatchConfigWithReady, which signals once the watch is registered. The test now waits for readiness, writes once, and asserts on the config handed to the callback. WatchConfig keeps its signature and delegates, so both production call sites are untouched. OnReload now documents the callback-before-publish ordering.

An earlier attempt used a write-retry loop; that was only less flaky, since coalesced events can still restart the 500ms debounce. The readiness signal removes the race instead of narrowing it.

Verified: 30x on the test, 3x full-suite -race, all clean. Baseline was 1 failure in 4 runs.

Patch dependency advisories

Both were within the existing tailwindcss constraint, so no breaking upgrade. npm audit is clean and the checked-in compiled-tailwind.css is byte-identical after a rebuild.

Adds .github/dependabot.yml covering gomod, npm and github-actions — the unreported nanoid advisory is exactly the gap it closes.

Fedora RPM builds

x86_64 and aarch64 RPMs on both release channels, built with pinned nfpm.

  • Packaged and verified in a dedicated rpm job on ubuntu-latest, then handed to release as an artifact. They still enter the single atomic gh release create, because this repo publishes immutable releases and rejects assets added afterwards.
  • Verification asserts name, version, architecture, license, every payload path, the noreplace flag on the config, and that the packaged binary is an executable ELF of the right machine type. It collects all failures before exiting.
  • The same scripts run in ci.yml on every PR, so packaging breaks at review time rather than at release time.
  • Fedora conventions: binary in /usr/bin, config in /etc marked noreplace so upgrades keep local edits, license in /usr/share/licenses, and an opt-in systemd user unit in /usr/lib/systemd/user. A system unit would wrongly imply a machine-level daemon for a loopback developer proxy.
  • make rpm builds locally.

Not included, deliberately: GPG signing, COPR, EPEL submission, SELinux policy. The docs say the RPMs are unsigned and point at checksums.txt.

Configure golangci-lint

It was invoked by the committed pre-push hook but had no config and never ran in CI. Adds a .golangci.yml (v2 schema) that passes with zero issues: the standard set plus bodyclose, copyloopvar, misspell, nolintlint, whitespace, and revive with an explicit 23-rule list. gosec (25 findings), errorlint and gocritic were evaluated and rejected — their findings are either inherent to a local CLI or need code changes, and blanket-suppressing them would make the linter meaningless. Adds make lint-strict and a SHA-pinned CI lint action; make lint stays the fast gofmt+vet check.

Routing reasons read from config

The Reason strings in scenarios.go hardcoded model names that had drifted (GLM-5.1, Kimi K2.6). Root cause: ScenarioResult.Reason had zero production consumers, so nothing ever displayed the drift.

DetectScenario now explains only why a scenario matched; ModelRouter appends the resolved model where resolution actually happens — covering scenario, cost-based catalog selection, respect_requested_model and both override paths. The routing log line carries it, and a test fails if a detector reason ever names a model again. Also fixes ScenarioPolicy.Evaluate, which was formatting the scenario name twice as a placeholder instead of using the reason.

Documentation correctness pass

Every doc was audited against the source. The errors were real, not cosmetic:

  • Long-context threshold is 100K, not 80K
  • The vision tier was missing from the documented scenario priority (it fires second)
  • Four endpoint types, not two; the Anthropic endpoint covers Qwen as well as MiniMax
  • MiniMax M2.5 is 200K, not 1M — so long-context advice now points at MiniMax M3, which is what the shipped config uses
  • All four Qwen3.x models are 1M, not ~128K, and are vision-capable
  • Kimi vision was undocumented; gpt-5.5-mini/nano were missing
  • A documented fallback chain routed long-context requests through a 200K model
  • config.example.json was missing six real config blocks, including all of OpenRouter
  • A documented request_dedup option does not exist in the code (removed); three analytics routes do (documented)
  • INSTALLATION.md never mentioned the RPMs, and the macOS DMG was documented only in Chinese

docs/models.md was a second, thinner copy of MODELS.md added by #116. It is now a pointer, because the duplication had already caused drift: an earlier pass corrected the capability numbers in one file and left the same errors in the other.

Chinese translations of CONFIGURATION, MODELS and INSTALLATION are brought up to date with their English originals; English TROUBLESHOOTING gained the sections that existed only in Chinese.

Verification

  • go test ./... -race clean (repeated), go vet clean, golangci-lint 0 issues, gofmt clean
  • npm audit clean
  • All workflow/config YAML and JSON parse; job dependency graph checked
  • 0 broken relative links across all docs
  • RPMs built and inspected locally for both arches: correct NEVRA, AGPL-3.0-only matching LICENSE, FHS layout, cn (config|noreplace) on the config, and a static ELF that runs and reports its version
  • The RPM verification script was confirmed non-vacuous — it exits 1 with the correct errors when fed a mismatched architecture

Known limitations

  • Not verifiable until a release actually runs: the artifact handoff between the rpm and release jobs. The PR-time ci.yml job exercises the build and verification, but not the cross-job transfer.
  • The RPMs are unsigned. Minimum next step would be a project GPG key or COPR.
  • docs/zh/TROUBLESHOOTING.md is still ahead of English in places beyond what was ported; the remaining zh drift is noted rather than silently reconciled.

…uilds

Four pieces of maintenance and one new deliverable.

Flaky test — TestWatchConfig_DetectsFileChange failed roughly 1 in 4
full-suite race runs and blocked pushes via the pre-push hook. Two races
were involved: WatchConfig registers its fsnotify watch asynchronously, so
the test's sleep-then-write could lose the write entirely; and
AtomicConfig.Reload runs reload callbacks before publishing the new pointer,
so asserting via Get() could read the old config. Adds
WatchConfigWithReady, which signals once the watch is registered, letting
the test wait for readiness, write once, and assert on the config handed to
the callback. WatchConfig keeps its signature and delegates, so both
production call sites are unchanged. OnReload now documents the
callback-before-publish ordering.

Dependency advisories — postcss 8.5.18 to 8.5.26 (GHSA-fxqj-rqcc-2cmp,
Dependabot alert #2) and nanoid to 3.3.18 (GHSA-2v37-7h3g-55p8, high
severity, never alerted on). Both were in range of the existing
tailwindcss constraint, so no breaking upgrade. npm audit is clean and the
checked-in compiled-tailwind.css is byte-identical after a rebuild. Adds
.github/dependabot.yml covering gomod, npm and github-actions, since the
unreported nanoid advisory showed the gap.

Fedora packaging — builds x86_64 and aarch64 RPMs with nfpm (pinned
v2.47.0) and attaches them to both release channels. The RPMs are produced
inside the existing release job and passed to the single gh release create
call: this repo publishes immutable releases, so assets added afterwards
would be rejected. A verification step reads the ELF header directly
(magic, class, e_machine) rather than grepping file output, which words
things differently on macOS and would silently always pass. Package layout
follows Fedora conventions: binary in /usr/bin, config in /etc marked
noreplace so upgrades keep local edits, license in /usr/share/licenses,
and an opt-in systemd *user* unit in /usr/lib/systemd/user — a system unit
would wrongly imply a machine-level daemon for a loopback developer proxy.
Adds a make rpm target for local inspection, since rpm -qip is unavailable
on the macOS runner.

Docs — corrections found by auditing every doc against the source:
the long-context threshold is 100K not 80K, the vision tier was missing
from the documented scenario priority, there are four endpoint types not
two, the Anthropic endpoint covers Qwen as well as MiniMax, model tables
now match modelMetadata, the scenario defaults now match the shipped
default_config.json, OpenRouter's API-key env vars were undocumented, and
config.example.json gained the six real config blocks it was missing.
Removes a documented request_dedup option that does not exist in the code
and documents the three analytics routes that do. Fixes stale beta-version
formats and broken links, and flags the zh translations that lag.
Follow-ups on the four decisions taken after the previous commit.

Routing reasons now read the resolved model from config. The Reason strings
in scenarios.go hardcoded model names that had drifted (GLM-5.1, Kimi K2.6)
because ScenarioResult.Reason had zero production consumers — nothing ever
displayed them, so nobody noticed. DetectScenario now explains only why a
scenario matched, and ModelRouter appends the model at the point it is
actually resolved, covering scenario, cost-based catalog selection,
respect_requested_model and both override paths. The routing log line now
carries it, and a test fails if a detector reason ever names a model again.
Also fixes ScenarioPolicy.Evaluate, which was formatting the scenario name
twice as a placeholder instead of using the reason.

golangci-lint is now configured rather than implied. Adds a .golangci.yml
in v2 schema that passes with zero issues on the current tree: the standard
set plus bodyclose, copyloopvar, misspell, nolintlint, whitespace, and
revive with an explicit 23-rule list. gosec, errorlint and gocritic were
evaluated and rejected — their findings are either inherent to a local CLI
or need code changes, and suppressing them wholesale would make the linter
meaningless. Adds make lint-strict, pins the CI lint action by SHA, and the
committed pre-push hook now picks the config up with no change. make lint
stays as the fast gofmt+vet check.

RPM packaging moves to a dedicated Linux job so CI can actually inspect the
packages. rpm runs on ubuntu-latest before release and hands over the
rpm-packages artifact, which still enters the single atomic gh release
create — immutable releases forbid adding assets afterwards. Verification
asserts name, version, architecture, license, every payload path, the
noreplace flag on the config, and that the packaged binary is an executable
ELF of the right machine type; it collects all failures before exiting. The
same scripts run in ci.yml on every PR, so packaging breaks at review time
rather than at release time.

Documentation. docs/models.md was a second, thinner copy of MODELS.md
introduced by #116; it is now a pointer, because the duplication had already
caused real drift — an earlier pass corrected the capability numbers in one
file and left the same errors in the other. Those errors are fixed: MiniMax
M2.5 is 200K not 1M (so long-context advice now points at MiniMax M3, which
is what the shipped config uses), all four Qwen3.x models are 1M not 128K
and are vision-capable, Kimi vision was undocumented, gpt-5.5-mini/nano were
missing, and a documented fallback chain routed long context through a 200K
model. The Chinese translations of CONFIGURATION, MODELS and INSTALLATION
are brought up to date with their English originals, and English
TROUBLESHOOTING gains the sections that only existed in Chinese.
…I installs

The Kimi K2.6 row in the cost comparison table said ~1,150 requests per $12
while its own profile, the decision tree and the quick reference all said
1,850. Nothing in the repo can settle it — the model catalog carries no rate
data for any of its 353 models — but the row duplicated GLM-5's figure
exactly, which reads as a copy-paste, and 1,850 is what the other three
places and the model's value rating agree on. Both files now say 1,850, and
the table gained a note that these figures are indicative estimates rather
than a machine-readable price list, so the next reader knows not to budget
against them.

INSTALLATION.md documented Homebrew, Scoop, source, raw binaries and Docker
but never mentioned the RPMs, even though it is the canonical install doc
that README points at. It now covers dnf installation, the noreplace config
template, the opt-in systemd user unit, the missing GPG signature, and the
fact that `routatic-proxy update` is for standalone binaries rather than RPM
installs.

The macOS DMG was documented only in Chinese. Ported to English, describing
what the menu bar item actually offers per internal/tray/tray_darwin.go —
status, Open Console, start/stop, and Start on Boot — with the CLI
equivalents. The Chinese install guide gained the RPM section in turn, so
the two stay in sync.
The RPM Packaging job failed on ubuntu-latest with "cpio: cannot make
directory '/etc/routatic-proxy': Permission denied". RPM payload members are
absolute paths, and whether cpio strips the leading "/" by default differs
between distributions — Fedora's does, Ubuntu's does not, so the extraction
step aimed at the runner's real /usr and /etc instead of the temp dir.

Passing --no-absolute-filenames makes it explicit rather than relying on the
local default. This is why the job exists: the same script passed on a Fedora
workstation and only failed on a real Linux CI runner.
@kilo-code-bot

kilo-code-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything.

📊 Overall: Like finding a unicorn in production — I didn't think clean PRs existed anymore, but here we are.

Files Reviewed (9 files)
  • internal/config/atomic.go - doc comment
  • internal/config/watcher.go - readiness signal
  • internal/config/watcher_test.go - flaky test fix
  • internal/handlers/messages.go - log reason field
  • internal/router/model_router.go - RouteResult.Reason
  • internal/router/model_router_test.go - new routing reason tests
  • internal/router/policy.go - double-formatting bug fix
  • internal/router/scenarios.go - reason string cleanup
  • internal/router/scenarios_test.go - updated reason assertions
Previous Review Summary (commit 82b2e1d)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 82b2e1d)

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything.

📊 Overall: Like finding a unicorn in production — I didn't think clean PRs existed anymore, but here we are.

Files Reviewed (9 files)
  • internal/config/atomic.go - doc comment
  • internal/config/watcher.go - readiness signal
  • internal/config/watcher_test.go - flaky test fix
  • internal/handlers/messages.go - log reason field
  • internal/router/model_router.go - RouteResult.Reason
  • internal/router/model_router_test.go - new routing reason tests
  • internal/router/policy.go - double-formatting bug fix
  • internal/router/scenarios.go - reason string cleanup
  • internal/router/scenarios_test.go - updated reason assertions

Reviewed by free · Input: 184.5K · Output: 13.8K · Cached: 141.5K

Review guidance: REVIEW.md from base branch main (truncated)

Ubuntu's rpm2cpio exits 1 even when it has written the payload in full, while
Fedora's exits 0. Under `set -o pipefail` that sank the whole extraction
pipeline, so the RPM job failed on ubuntu-latest immediately after the
previous fix let cpio run at all.

The status we care about is cpio's, so the pipeline runs with pipefail off and
its subshell status is captured instead. Because that means neither exit code
is fully trusted, the script now also compares the extracted binary's size
against the size recorded in the RPM header — a mid-stream extraction failure
would otherwise leave a truncated binary whose ELF header still looks valid.

Reproduced and fixed against ubuntu:24.04 in a container rather than guessing:
both arches now pass on Ubuntu and Fedora, and the wrong-arch and
wrong-version negative controls still exit 1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant