mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-04-07 02:46:50 +00:00
ref(gitea): rewrite as standalone fetcher, not a githubish wrapper
Gitea's API is similar to GitHub's but not identical (different URL prefix, limit vs per_page, token auth header). Give it its own types and pagination logic rather than coupling through githubish.
This commit is contained in:
@@ -1,24 +1,120 @@
|
||||
// Package gitea fetches releases from a Gitea (or Forgejo) instance.
|
||||
// Package gitea fetches releases from a Gitea or Forgejo instance.
|
||||
//
|
||||
// Gitea's release API is GitHub-compatible but lives under /api/v1:
|
||||
// Gitea's release API lives under:
|
||||
//
|
||||
// GET {baseurl}/api/v1/repos/{owner}/{repo}/releases
|
||||
//
|
||||
// This package appends the /api/v1 prefix and delegates to [githubish].
|
||||
// The response shape is similar to GitHub's but not identical. This package
|
||||
// handles pagination, authentication, and deserialization independently.
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"iter"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/webinstall/webi-installers/internal/releases/githubish"
|
||||
)
|
||||
|
||||
// Fetch retrieves releases from a Gitea instance.
|
||||
// The baseURL should be the Gitea root (e.g. "https://git.rootprojects.org"),
|
||||
// not the API path — /api/v1 is appended automatically.
|
||||
func Fetch(ctx context.Context, client *http.Client, baseURL, owner, repo string, auth *githubish.Auth) iter.Seq2[[]githubish.Release, error] {
|
||||
return githubish.Fetch(ctx, client, strings.TrimRight(baseURL, "/")+"/api/v1", owner, repo, auth)
|
||||
// Release is one release from the Gitea releases API.
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
Draft bool `json:"draft"`
|
||||
PublishedAt string `json:"published_at"` // "2023-11-05T06:38:05Z"
|
||||
Assets []Asset `json:"assets"`
|
||||
TarballURL string `json:"tarball_url"`
|
||||
ZipballURL string `json:"zipball_url"`
|
||||
}
|
||||
|
||||
// Asset is one downloadable file attached to a release.
|
||||
type Asset struct {
|
||||
Name string `json:"name"` // "pathman-v0.6.0-darwin-amd64.tar.gz"
|
||||
BrowserDownloadURL string `json:"browser_download_url"` // full URL
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// Auth holds optional credentials for authenticated API access.
|
||||
type Auth struct {
|
||||
Token string // personal access token or API key
|
||||
}
|
||||
|
||||
// Fetch retrieves releases from a Gitea instance, paginating automatically.
|
||||
// Each yield is one page of releases.
|
||||
//
|
||||
// The baseURL should be the Gitea root (e.g. "https://git.rootprojects.org").
|
||||
// The /api/v1 prefix is appended automatically.
|
||||
func Fetch(ctx context.Context, client *http.Client, baseURL, owner, repo string, auth *Auth) iter.Seq2[[]Release, error] {
|
||||
return func(yield func([]Release, error) bool) {
|
||||
base := strings.TrimRight(baseURL, "/")
|
||||
page := 1
|
||||
|
||||
for {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases?limit=50&page=%d",
|
||||
base, owner, repo, page)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
yield(nil, fmt.Errorf("gitea: %w", err))
|
||||
return
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if auth != nil && auth.Token != "" {
|
||||
req.Header.Set("Authorization", "token "+auth.Token)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
yield(nil, fmt.Errorf("gitea: fetch %s: %w", url, err))
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
yield(nil, fmt.Errorf("gitea: fetch %s: %s", url, resp.Status))
|
||||
return
|
||||
}
|
||||
|
||||
var releases []Release
|
||||
err = json.NewDecoder(resp.Body).Decode(&releases)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
yield(nil, fmt.Errorf("gitea: decode %s: %w", url, err))
|
||||
return
|
||||
}
|
||||
|
||||
if !yield(releases, nil) {
|
||||
return
|
||||
}
|
||||
|
||||
// Gitea uses Link headers like GitHub for pagination.
|
||||
if nextURL := nextPageURL(resp.Header.Get("Link")); nextURL != "" {
|
||||
url = nextURL
|
||||
page++ // not strictly needed since we follow the URL, but keeps logic clear
|
||||
continue
|
||||
}
|
||||
|
||||
// No next link — also stop if we got fewer results than requested.
|
||||
if len(releases) < 50 {
|
||||
return
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var reNextLink = regexp.MustCompile(`<([^>]+)>;\s*rel="next"`)
|
||||
|
||||
func nextPageURL(link string) string {
|
||||
if link == "" {
|
||||
return ""
|
||||
}
|
||||
m := reNextLink.FindStringSubmatch(link)
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
return m[1]
|
||||
}
|
||||
|
||||
107
internal/releases/gitea/gitea_test.go
Normal file
107
internal/releases/gitea/gitea_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package gitea_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/webinstall/webi-installers/internal/releases/gitea"
|
||||
)
|
||||
|
||||
const testReleases = `[
|
||||
{
|
||||
"tag_name": "v0.6.0",
|
||||
"name": "v0.6.0",
|
||||
"prerelease": false,
|
||||
"draft": false,
|
||||
"published_at": "2023-11-05T06:38:05Z",
|
||||
"tarball_url": "https://example.com/archive/v0.6.0.tar.gz",
|
||||
"zipball_url": "https://example.com/archive/v0.6.0.zip",
|
||||
"assets": [
|
||||
{
|
||||
"name": "tool-v0.6.0-linux-amd64.tar.gz",
|
||||
"browser_download_url": "https://example.com/releases/download/v0.6.0/tool-v0.6.0-linux-amd64.tar.gz",
|
||||
"size": 89215
|
||||
}
|
||||
]
|
||||
}
|
||||
]`
|
||||
|
||||
func TestFetch(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/repos/root/tool/releases" {
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(testReleases))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
var all []gitea.Release
|
||||
|
||||
for releases, err := range gitea.Fetch(ctx, srv.Client(), srv.URL, "root", "tool", nil) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all = append(all, releases...)
|
||||
}
|
||||
|
||||
if len(all) != 1 {
|
||||
t.Fatalf("got %d releases, want 1", len(all))
|
||||
}
|
||||
if all[0].TagName != "v0.6.0" {
|
||||
t.Errorf("TagName = %q, want %q", all[0].TagName, "v0.6.0")
|
||||
}
|
||||
if len(all[0].Assets) != 1 {
|
||||
t.Errorf("got %d assets, want 1", len(all[0].Assets))
|
||||
}
|
||||
if all[0].TarballURL == "" {
|
||||
t.Error("TarballURL is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAuth(t *testing.T) {
|
||||
var gotAuth string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
w.Write([]byte("[]"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
auth := &gitea.Auth{Token: "abc123"}
|
||||
for _, err := range gitea.Fetch(ctx, srv.Client(), srv.URL, "root", "tool", auth) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if gotAuth != "token abc123" {
|
||||
t.Errorf("Authorization = %q, want %q", gotAuth, "token abc123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLive(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping network test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client := &http.Client{}
|
||||
|
||||
var total int
|
||||
for releases, err := range gitea.Fetch(ctx, client, "https://git.rootprojects.org", "root", "pathman", nil) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
total += len(releases)
|
||||
}
|
||||
|
||||
if total < 1 {
|
||||
t.Errorf("got %d releases, expected at least 1", total)
|
||||
}
|
||||
t.Logf("fetched %d releases", total)
|
||||
}
|
||||
Reference in New Issue
Block a user