Files
headscale/tools/bump/github.go
T
Kristoffer Dalby 60c5f8277f .github: fold the action version updater into tools/bump
The standalone workflow has failed weekly since its token secret went
missing. One bot, one credential, one pull request to review.
2026-09-25 15:52:26 +02:00

132 lines
3.3 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"sync"
)
const githubAPI = "https://api.github.com"
var errNoRelease = errors.New("no published release")
// githubToken is resolved once. Unauthenticated GitHub allows sixty requests an
// hour, which the action-pin sweep alone would exhaust, so a token is worth
// looking for even outside CI.
var githubToken = sync.OnceValue(func() string {
for _, env := range []string{"GH_TOKEN", "GITHUB_TOKEN"} {
if v := os.Getenv(env); v != "" {
return v
}
}
out, err := exec.Command("gh", "auth", "token").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
})
// githubJSON performs an authenticated GET against the GitHub REST API.
func githubJSON(ctx context.Context, path string, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, githubAPI+path, nil)
if err != nil {
return fmt.Errorf("building request for %s: %w", path, err)
}
req.Header.Set("Accept", "application/vnd.github+json")
if token := githubToken(); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
client := &http.Client{Timeout: proxyTimeout}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("GET %s: %w", path, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GET %s: %w: %s", path, errHTTPStatus, resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading %s: %w", path, err)
}
if err := json.Unmarshal(body, out); err != nil { //nolint:noinlineerr
return fmt.Errorf("decoding %s: %w", path, err)
}
return nil
}
// latestRelease is the tag of a repository's newest published release.
func latestRelease(ctx context.Context, owner, name string) (string, error) {
var release struct {
TagName string `json:"tag_name"`
}
err := githubJSON(ctx, fmt.Sprintf("/repos/%s/%s/releases/latest", owner, name), &release)
if err != nil {
return "", err
}
if release.TagName == "" {
return "", fmt.Errorf("%w for %s/%s", errNoRelease, owner, name)
}
return release.TagName, nil
}
// commitOfRef resolves a tag or branch to the commit it points at, following
// annotated tags the way an action pin must.
func commitOfRef(ctx context.Context, owner, name, ref string) (string, error) {
var commit struct {
SHA string `json:"sha"`
}
err := githubJSON(ctx, fmt.Sprintf("/repos/%s/%s/commits/%s", owner, name, ref), &commit)
if err != nil {
return "", err
}
if commit.SHA == "" {
return "", fmt.Errorf("%w: %s/%s@%s", errNoRelease, owner, name, ref)
}
return commit.SHA, nil
}
// currentSlug is the repository the run is acting on. It defaults to whatever
// the job is running in rather than a constant: a hardcoded upstream slug let a
// fork push its branch and then try to open the pull request on someone else's
// repository, which fails at the very last step of a long run.
func currentSlug(ctx context.Context, r *repo, override string) (string, error) {
if override != "" {
return override, nil
}
if env := os.Getenv("GITHUB_REPOSITORY"); env != "" {
return env, nil
}
out, err := r.run(ctx, "gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner")
if err != nil {
return "", err
}
return strings.TrimSpace(out), nil
}