package selfupdateimport ( "encoding/json" "fmt" "io" "net/http" "os" "strings")// Repo is the GitHub repository release assets are published to.const Repo = "davis7dotsh/tx9"// ErrNoRelease indicates the repository has no published releases yet.// GitHub's /releases/latest returns 404 in that case, which is the// expected state until the first `v*` tag is pushed (see// .github/workflows/release.yml) — callers should surface this as a// clean, actionable message rather than a raw HTTP error.var ErrNoRelease = fmt.Errorf("no releases published yet")// Asset is one file attached to a GitHub release.type Asset struct { Name string `json:"name"` BrowserDownloadURL string `json:"browser_download_url"`}// Release is the subset of the GitHub releases API response this package// needs.type Release struct { TagName string `json:"tag_name"` Assets []Asset `json:"assets"`}// FindAsset returns the asset in r named name, or false if absent.func (r *Release) FindAsset(name string) (Asset, bool) { for _, a := range r.Assets { if a.Name == name { return a, true } } return Asset{}, false}func (r *Release) assetNames() string { names := make([]string, len(r.Assets)) for i, a := range r.Assets { names[i] = a.Name } return strings.Join(names, ", ")}// FetchLatestRelease queries the GitHub API for Repo's latest release. It// honors the GITHUB_TOKEN environment variable (if set) as a bearer// credential on the API call only, to avoid the low unauthenticated rate// limit — it is deliberately NOT attached to asset download requests,// since browser_download_url responses are signed redirects that an extra// Authorization header can break.func FetchLatestRelease(httpClient *http.Client) (*Release, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", Repo) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("selfupdate: build request: %w", err) } req.Header.Set("User-Agent", userAgent) req.Header.Set("Accept", "application/vnd.github+json") if tok := os.Getenv("GITHUB_TOKEN"); tok != "" { req.Header.Set("Authorization", "Bearer "+tok) } resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("selfupdate: fetch latest release: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, ErrNoRelease } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return nil, fmt.Errorf("selfupdate: GitHub API returned %s: %s", resp.Status, strings.TrimSpace(string(body))) } var rel Release if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil { return nil, fmt.Errorf("selfupdate: decode release response: %w", err) } if rel.TagName == "" { return nil, fmt.Errorf("selfupdate: release response missing tag_name") } return &rel, nil}
package selfupdate
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)
// Repo is the GitHub repository release assets are published to.
const Repo = "davis7dotsh/tx9"
// ErrNoRelease indicates the repository has no published releases yet.
// GitHub's /releases/latest returns 404 in that case, which is the
// expected state until the first `v*` tag is pushed (see
// .github/workflows/release.yml) — callers should surface this as a
// clean, actionable message rather than a raw HTTP error.
var ErrNoRelease = fmt.Errorf("no releases published yet")
// Asset is one file attached to a GitHub release.
type Asset struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
}
// Release is the subset of the GitHub releases API response this package
// needs.
type Release struct {
TagName string `json:"tag_name"`
Assets []Asset `json:"assets"`
}
// FindAsset returns the asset in r named name, or false if absent.
func (r *Release) FindAsset(name string) (Asset, bool) {
for _, a := range r.Assets {
if a.Name == name {
return a, true
}
}
return Asset{}, false
}
func (r *Release) assetNames() string {
names := make([]string, len(r.Assets))
for i, a := range r.Assets {
names[i] = a.Name
}
return strings.Join(names, ", ")
}
// FetchLatestRelease queries the GitHub API for Repo's latest release. It
// honors the GITHUB_TOKEN environment variable (if set) as a bearer
// credential on the API call only, to avoid the low unauthenticated rate
// limit — it is deliberately NOT attached to asset download requests,
// since browser_download_url responses are signed redirects that an extra
// Authorization header can break.
func FetchLatestRelease(httpClient *http.Client) (*Release, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", Repo)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("selfupdate: build request: %w", err)
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "application/vnd.github+json")
if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
req.Header.Set("Authorization", "Bearer "+tok)
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("selfupdate: fetch latest release: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, ErrNoRelease
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return nil, fmt.Errorf("selfupdate: GitHub API returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
var rel Release
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return nil, fmt.Errorf("selfupdate: decode release response: %w", err)
}
if rel.TagName == "" {
return nil, fmt.Errorf("selfupdate: release response missing tag_name")
}
return &rel, nil
}