initial commit [141.14.140.180,mike]
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user