From f15868d442380f9b74fa3787ef0f4d4d7e2bb1bd Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Thu, 16 Jul 2026 15:21:36 +0530 Subject: [PATCH] fix(packages): serve noarch Alpine index for any requested architecture (#38479) Fixes #38456 ## Problem The Alpine package registry serves one `APKINDEX.tar.gz` per architecture. When a repository contains only `noarch` packages (no architecture-specific packages), only the `noarch` index is built. Because `apk` substitutes `$ARCH` with the host architecture and requests e.g. `x86_64/APKINDEX.tar.gz`, such a repository returned HTTP 404 and was unusable, matching the report in #38456. ## Fix `GetRepositoryFile` now falls back to the `noarch` index when the requested architecture has no index of its own, mirroring the fallback already present in the sibling `DownloadPackageFile` handler. `noarch` packages are installable on every architecture, so serving them for any requested architecture is correct. The index-build side is unchanged; only the serving path gains the fallback, so mixed repositories (which already merge `noarch` into each per-architecture index) are unaffected. ## AI assistance disclosure This change was implemented with the help of an AI coding assistant, which gitea's CONTRIBUTING.md explicitly welcomes when disclosed. I have reviewed the change, understand it, and can explain and defend it. ## Tests Added a `NoArchOnly` subtest to `TestPackageAlpine` that publishes only a `noarch` package to a fresh repository and asserts that `GET .../x86_64/APKINDEX.tar.gz` now returns `200` (previously `404`) and that the served index lists the noarch package. Verified locally with `go build`/`go vet` on the changed package and a compile of the integration test package (`go test -c`); the full integration run relies on CI. --------- Signed-off-by: wxiaoguang Co-authored-by: wxiaoguang --- routers/api/packages/alpine/alpine.go | 21 ++++++++++++- tests/integration/api_packages_alpine_test.go | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index d3560dc4b99..9a593f7caa2 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -67,15 +67,34 @@ func GetRepositoryFile(ctx *context.Context) { return } + branch := ctx.PathParam("branch") + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ Filename: alpine_service.IndexArchiveFilename, - CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.PathParam("branch"), ctx.PathParam("repository"), ctx.PathParam("architecture")), + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), }, ctx.Req.Method, ) + // A repository that only contains "noarch" packages has no per-architecture + // index. Since noarch packages are installable on every architecture, fall + // back to the noarch index so clients requesting their own architecture + // (e.g. x86_64) can still discover them. + if errors.Is(err, util.ErrNotExist) && architecture != alpine_module.NoArch { + s, u, pf, err = packages_service.OpenFileForDownloadByPackageVersion( + ctx, + pv, + &packages_service.PackageFileInfo{ + Filename: alpine_service.IndexArchiveFilename, + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch), + }, + ctx.Req.Method, + ) + } if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/tests/integration/api_packages_alpine_test.go b/tests/integration/api_packages_alpine_test.go index 0fe158e7f04..5b097534e28 100644 --- a/tests/integration/api_packages_alpine_test.go +++ b/tests/integration/api_packages_alpine_test.go @@ -268,6 +268,36 @@ AACAX/AKARNTyAAoAAA=` AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) }) + + t.Run("NoArchOnly", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // A repository that only contains noarch packages has no per-architecture index, + // but apk always requests the index for its own architecture (e.g. x86_64). + // That request must fall back to the noarch index instead of 404ing. + noarchRepository := repository + "-noarchonly" + + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s/%s", rootURL, branch, noarchRepository), bytes.NewReader(noarchContent)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, noarchRepository)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + assert.Contains(t, content, "C:Q1kbH5WoIPFccQYyATanaKXd2cJcc=\n") + assert.Contains(t, content, "A:noarch\n") + + // The noarch index is still directly retrievable too. + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/noarch/APKINDEX.tar.gz", rootURL, branch, noarchRepository)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/noarch/gitea-noarch-1.4-r0.apk", rootURL, branch, noarchRepository)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) }) } }