initial commit [141.14.140.180,mike]

This commit is contained in:
2026-08-14 09:09:59 +02:00
commit 6a991a25a6
19 changed files with 6691 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"regexp"
"strings"
)
// Gitea and GitHub expose the same release JSON, only under different API
// roots and with different auth headers.
type forgeCtx struct {
base string // scheme, host and any sub-path Gitea is mounted under
owner string
repo string
forge string // "github" or "gitea"
token string
api string // .../repos/<owner>/<repo>
}
type Asset struct {
ID int64 `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
BrowserDownloadURL string `json:"browser_download_url"`
Digest string `json:"digest"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type Release struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
PublishedAt string `json:"published_at"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
Assets []Asset `json:"assets"`
}
func newForgeCtx(spec *Spec) (*forgeCtx, error) {
base, owner, repo, err := parseRepo(spec.Repo)
if err != nil {
return nil, err
}
forge, err := detectForge(base, spec.Forge)
if err != nil {
return nil, err
}
c := &forgeCtx{
base: base,
owner: owner,
repo: repo,
forge: forge,
token: resolveToken(forge, spec.Token),
}
c.api = apiBase(base, forge) + "/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(repo)
return c, nil
}
func parseRepo(repo string) (base, owner, name string, err error) {
repo = strings.TrimRight(repo, "/")
repo = strings.TrimSuffix(repo, ".git")
if !strings.HasPrefix(repo, "http://") && !strings.HasPrefix(repo, "https://") {
repo = "https://" + repo
}
u, e := url.Parse(repo)
if e != nil || u.Host == "" {
return "", "", "", fmt.Errorf("cannot parse repository URL: %s", repo)
}
// Anything before <owner>/<repo> is a base path (Gitea under a sub-path).
seg := strings.FieldsFunc(u.Path, func(r rune) bool { return r == '/' })
if len(seg) < 2 {
return "", "", "", fmt.Errorf("repository URL needs <owner>/<repo>: %s", repo)
}
owner, name = seg[len(seg)-2], seg[len(seg)-1]
base = u.Scheme + "://" + u.Host
if prefix := seg[:len(seg)-2]; len(prefix) > 0 {
base += "/" + strings.Join(prefix, "/")
}
return base, owner, name, nil
}
var githubHost = regexp.MustCompile(`(^|\.)github\.com$`)
func hostOf(base string) string {
u, err := url.Parse(base)
if err != nil {
return ""
}
return strings.ToLower(u.Hostname())
}
func detectForge(base, override string) (string, error) {
if override != "" {
f := strings.ToLower(override)
if f != "github" && f != "gitea" {
return "", fmt.Errorf("unknown forge %q (use github or gitea)", override)
}
return f, nil
}
if githubHost.MatchString(hostOf(base)) {
return "github", nil
}
return "gitea", nil
}
func apiBase(base, forge string) string {
if forge != "github" {
return base + "/api/v1"
}
if githubHost.MatchString(hostOf(base)) {
return "https://api.github.com"
}
return base + "/api/v3" // GitHub Enterprise
}
func resolveToken(forge, explicit string) string {
if explicit != "" {
return explicit
}
env := []string{"UPD_TOKEN", "GITEA_TOKEN"}
if forge == "github" {
env = []string{"UPD_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"}
}
for _, k := range env {
if v := os.Getenv(k); v != "" {
return v
}
}
return ""
}
func (c *forgeCtx) authHeaders() map[string]string {
if c.token == "" {
return map[string]string{}
}
// GitHub wants "Bearer", Gitea wants "token".
if c.forge == "github" {
return map[string]string{"Authorization": "Bearer " + c.token}
}
return map[string]string{"Authorization": "token " + c.token}
}
// Private GitHub assets are only reachable through the API URL; the browser
// URL redirects to storage, where the Authorization header is dropped.
func (c *forgeCtx) assetURL(a *Asset) (string, map[string]string) {
if c.forge == "github" && c.token != "" && a.ID != 0 {
return fmt.Sprintf("%s/releases/assets/%d", c.api, a.ID),
map[string]string{"Accept": "application/octet-stream"}
}
return a.BrowserDownloadURL, map[string]string{}
}
func (c *forgeCtx) releasesURL() string {
if c.forge == "github" {
return c.api + "/releases?per_page=50"
}
return c.api + "/releases?limit=50&draft=false"
}
// ==============================================================================
// Release selection
// ==============================================================================
func (c *forgeCtx) fetchRelease(spec *Spec) (*Release, []Release, error) {
// A specific tag: both forges have a direct endpoint. Fall back to the list
// so "26.5.6" also finds a tag named "v26.5.6".
if spec.Tag != "" {
body, err := c.apiGet(c.api+"/releases/tags/"+url.PathEscape(spec.Tag), true)
if err != nil {
return nil, nil, err
}
if body != nil {
var rel Release
if json.Unmarshal(body, &rel) == nil && rel.TagName != "" {
return &rel, nil, nil
}
}
list, err := c.releaseList()
if err != nil {
return nil, nil, err
}
for i := range list {
if list[i].TagName == spec.Tag {
return &list[i], nil, nil
}
}
for i := range list {
if normVer(list[i].TagName) == normVer(spec.Tag) {
return &list[i], nil, nil
}
}
return nil, nil, fmt.Errorf("release %q not found (--list shows all)", spec.Tag)
}
// The plain case is one request: the forge already knows its latest
// non-draft, non-prerelease release.
if !spec.Pre && !opt.list {
body, err := c.apiGet(c.api+"/releases/latest", true)
if err != nil {
return nil, nil, err
}
if body != nil {
var rel Release
if json.Unmarshal(body, &rel) == nil && rel.TagName != "" {
return &rel, nil, nil
}
}
verbose("no /releases/latest, falling back to the release list")
}
list, err := c.releaseList()
if err != nil {
return nil, nil, err
}
if len(list) == 0 {
return nil, nil, fmt.Errorf("no releases found in %s/%s", c.owner, c.repo)
}
if opt.list {
return nil, list, nil
}
for i := range list {
if !list[i].Draft && (spec.Pre || !list[i].Prerelease) {
return &list[i], nil, nil
}
}
return nil, nil, fmt.Errorf("no suitable release found (try --pre)")
}
func (c *forgeCtx) releaseList() ([]Release, error) {
body, err := c.apiGet(c.releasesURL(), false)
if err != nil {
return nil, err
}
var list []Release
if err := json.Unmarshal(body, &list); err != nil {
return nil, fmt.Errorf("release list from %s is not an array: %w", c.releasesURL(), err)
}
return list, nil
}
func normVer(v string) string {
v = strings.TrimSpace(v)
if v != "" && (v[0] == 'v' || v[0] == 'V') {
v = v[1:]
}
return strings.TrimSpace(v)
}