Give RoutingSource its second upstream so every non-pinned component now
resolves a real latest-stable: Velocity via PaperMC (already wired), and
felis-api, k3s and cloudflared via the GitHub REST API.
github.go queries /repos/{repo}/releases/latest (one request, rate-limit
friendly) and fails closed: a transport error, a non-200 status (404 = no
stable release), an undecodable body, a draft/prerelease flag, or an
unparseable / prerelease-parsing tag all return an error, never a zero
version. It sends the User-Agent GitHub requires (a UA-less request is
403'd) and tolerates the two live tag styles -- cloudflared's CalVer
"2026.6.1" and k3s's v-prefixed, build-tagged "v1.36.2+k3s1" -- while
String() keeps the raw tag for the report.
source.go routes sourceGitHub to it and drops the errGitHubNotWired stub;
velocity still routes to PaperMC.
Tests: github_test.go covers both tag styles, the User-Agent gate, and
fail-closed on 404 / prerelease-flag / unparseable tag, with fixtures
captured from api.github.com on 2026-07-05. runner_test.go now drives
PaperMC and GitHub through dual httptest servers end to end with no source
degrading to an error.
doc.go re-tiers the verification boundary: both release sources are now
built and live-grounded; the VersionGatherer's version-extraction core is
the next verifiable slice (logic over an exec seam, not pure I/O); the
genuine I/O remainder is the Notifier, Applier and felis update CLI/CronJob.
felis-api's coord is still a placeholder slug, so that component is dark at
runtime until a real repository is configured.
49 lines
1.7 KiB
Go
49 lines
1.7 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"felis.lolicon.best/internal/updates"
|
|
)
|
|
|
|
// RoutingSource is the production updates.ReleaseSource. updates.Run calls a single
|
|
// source for every non-pinned component, so this one dispatches each component to its
|
|
// configured upstream by the topology: the PaperMC Fill API for Velocity, and the
|
|
// GitHub Releases API for felis-api, k3s and cloudflared.
|
|
type RoutingSource struct {
|
|
routes map[string]Spec
|
|
paper paperMC
|
|
gh github
|
|
}
|
|
|
|
// NewRoutingSource builds the router from a topology. Pinned specs are indexed too
|
|
// but never reached (Run skips pinned components before calling Latest).
|
|
func NewRoutingSource(specs []Spec) *RoutingSource {
|
|
routes := make(map[string]Spec, len(specs))
|
|
for _, s := range specs {
|
|
routes[s.Name] = s
|
|
}
|
|
return &RoutingSource{routes: routes, paper: newPaperMC(), gh: newGitHub()}
|
|
}
|
|
|
|
// Latest implements updates.ReleaseSource. An unknown component name is an error, not
|
|
// a silent zero, so a topology/route mismatch is loud.
|
|
func (r *RoutingSource) Latest(ctx context.Context, comp updates.Component) (updates.Version, error) {
|
|
spec, ok := r.routes[comp.Name]
|
|
if !ok {
|
|
return updates.Version{}, fmt.Errorf("updater: no source route for %q", comp.Name)
|
|
}
|
|
switch spec.Source {
|
|
case sourcePaperMC:
|
|
return r.paper.latestStable(ctx, spec.Coord)
|
|
case sourceGitHub:
|
|
return r.gh.latestStable(ctx, spec.Coord)
|
|
case sourceNone:
|
|
// A pinned component (Run never reaches this, but be explicit and loud).
|
|
return updates.Version{}, fmt.Errorf("updater: %q is pinned and has no release source", comp.Name)
|
|
default:
|
|
return updates.Version{}, fmt.Errorf("updater: component %q has an unknown source kind", comp.Name)
|
|
}
|
|
}
|