From 8d78d245b23bbdf77d1e9370727f204efcdf191b Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Fri, 2 Oct 2026 18:12:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(updater):=20=E5=AE=98=E6=96=B9=E4=BB=93?= =?UTF-8?q?=E5=BA=93=20404=20=E4=B8=8D=E5=86=8D=E6=8F=90=E7=A4=BA=E7=A7=81?= =?UTF-8?q?=E6=9C=89=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/updater/github.go | 10 +++++++--- internal/updater/github_test.go | 14 ++++++++++++++ internal/updater/topology.go | 11 ++++++----- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/internal/updater/github.go b/internal/updater/github.go index 2ddfd94..812ad9f 100644 --- a/internal/updater/github.go +++ b/internal/updater/github.go @@ -45,7 +45,8 @@ type github struct { // from a private fork, where its absence does not look like an auth failure: // GitHub answers 404 — not 401 or 403 — for a private repo the caller cannot see, so // "no token" is indistinguishable from "no release published yet" by status alone. - // latestStable says both in the error rather than making an operator guess. + // latestStable says both in the error rather than making an operator guess, for any + // repository other than the official one, which is public. // // It is read from the environment and never compiled in. A constant would be // committed to the very repository it protects, ship inside every felis binary where @@ -56,7 +57,7 @@ type github struct { } // tokenEnv names the environment variable holding the GitHub credential. deploy/bootstrap.sh -// reads the same variable to clone a private repo, so an operator sets one value once. +// reads the same variable to clone a private fork, so an operator sets one value once. const tokenEnv = "FELIS_GITHUB_TOKEN" // newGitHub builds a source pointed at the live GitHub REST API with sane defaults. @@ -148,7 +149,10 @@ func (g github) latestTag(ctx context.Context, repo string) (string, error) { // 404 is the ambiguous one: GitHub hides a private repo behind it rather than // answering 401/403, so an unauthenticated miss and a repo with no stable release // are the same status. Name both causes, and name the fix for the one an operator - // can act on. + // can act on. The official repository is public, so there only the first applies. + if resp.StatusCode == http.StatusNotFound && strings.EqualFold(repo, officialRepo) { + return "", fmt.Errorf("github: %s releases/latest returned HTTP 404 — it has no published stable release", repo) + } if resp.StatusCode == http.StatusNotFound && g.token == "" { return "", fmt.Errorf( "github: %s releases/latest returned HTTP 404 — either it has no published stable release, or it is private and %s is unset", diff --git a/internal/updater/github_test.go b/internal/updater/github_test.go index 7066b77..f492c06 100644 --- a/internal/updater/github_test.go +++ b/internal/updater/github_test.go @@ -223,4 +223,18 @@ func TestGitHubNamesTheTokenOnAnUnauthenticated404(t *testing.T) { if strings.Contains(err.Error(), tokenEnv) { t.Errorf("a 404 WITH a token set must not blame the missing token; got: %v", err) } + + // The official repository is public: a 404 there means no stable release, whatever + // the token. + g.token = "" + _, err = g.latestStable(context.Background(), "felismc/felis") + if err == nil { + t.Fatal("want an error on 404") + } + if strings.Contains(err.Error(), tokenEnv) || strings.Contains(err.Error(), "private") { + t.Errorf("a 404 from the public official repository must not suggest it is private; got: %v", err) + } + if !strings.Contains(err.Error(), "no published stable release") { + t.Errorf("a 404 from the official repository must say it has no stable release; got: %v", err) + } } diff --git a/internal/updater/topology.go b/internal/updater/topology.go index e371875..70c7c8c 100644 --- a/internal/updater/topology.go +++ b/internal/updater/topology.go @@ -13,6 +13,9 @@ const ( sourcePostgres // postgresql.org/versions.json, within Current's major ) +// officialRepo is Felis's own GitHub repository, which is public. +const officialRepo = "FelisMC/Felis" + // Spec is one platform component's static update policy plus how to find its latest // upstream version. Current is deliberately NOT here — it is gathered at runtime // (integration: an image tag, `k3s --version`, a jar manifest) and combined with the @@ -56,11 +59,9 @@ type Spec struct { func Topology() []Spec { return []Spec{ // Felis's own release repo, and the same slug deploy/bootstrap.sh clones from - // (FELIS_REPO_URL). It is PRIVATE today, which is why the github source carries an - // optional token: unauthenticated, this coord answers 404 — the status GitHub uses - // to hide a repo's existence — and felis-api is the one tracked component where - // that happens. k3s and cloudflared are public and need no credential. - {Name: "felis-api", Policy: updates.PolicyScheduled, Manageable: true, Source: sourceGitHub, Coord: "FelisMC/Felis"}, + // (FELIS_REPO_URL). It is public, like k3s and cloudflared; the github source's + // optional token is for a build whose coord names a private fork. + {Name: "felis-api", Policy: updates.PolicyScheduled, Manageable: true, Source: sourceGitHub, Coord: officialRepo}, {Name: "k3s", Policy: updates.PolicyNotify, Manageable: false, Source: sourceGitHub, Coord: "k3s-io/k3s"}, {Name: "cloudflared", Policy: updates.PolicyScheduled, Manageable: true, Source: sourceGitHub, Coord: "cloudflare/cloudflared"}, {Name: "velocity", Policy: updates.PolicyNotify, Manageable: false, Source: sourcePaperMC, Coord: "velocity"},