Files
KenanZhu fec3ace600
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s
chore: migrate mail themes to shared framework and locked upstream inputs
2026-10-09 18:34:36 +08:00

165 lines
4.4 KiB
Go

package upstream
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
var tagPattern = regexp.MustCompile(`^v([0-9]+)\.[0-9]+\.[0-9]+$`)
func validTag(tag string) bool {
m := tagPattern.FindStringSubmatch(tag)
if m == nil {
return false
}
major, err := strconv.Atoi(m[1])
return err == nil && major >= 28
}
// Sync downloads by resolved commit, validates in memory, then swaps the whole
// snapshot. Network/adapter failures leave the existing snapshot untouched.
func Sync(ctx context.Context, tag, dir string) error {
return syncFrom(ctx, tag, dir, "https://api.github.com/repos/go-gitea/gitea/", "https://raw.githubusercontent.com/go-gitea/gitea/")
}
func syncFrom(ctx context.Context, tag, dir, api, raw string) error {
if !validTag(tag) {
return fmt.Errorf("expected a stable Gitea 28+ tag, e.g. v28.0.0")
}
client := &http.Client{Timeout: 45 * time.Second}
get := func(url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "GiteaMailTemplates-snapshot")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("%s: HTTP %d", url, resp.StatusCode)
}
b, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
return b, err
}
b, err := get(api + "commits/" + tag)
if err != nil {
return err
}
var commit struct {
SHA string `json:"sha"`
Commit struct {
Tree struct {
SHA string `json:"sha"`
} `json:"tree"`
} `json:"commit"`
}
if err := json.Unmarshal(b, &commit); err != nil {
return err
}
b, err = get(api + "git/trees/" + commit.Commit.Tree.SHA + "?recursive=1")
if err != nil {
return err
}
var tree struct {
Truncated bool `json:"truncated"`
Tree []struct {
Path string `json:"path"`
Type string `json:"type"`
} `json:"tree"`
}
if err := json.Unmarshal(b, &tree); err != nil {
return err
}
if tree.Truncated {
return fmt.Errorf("upstream tree is truncated")
}
s := &Snapshot{Lock: Lock{Schema: 1, Repository: "https://github.com/go-gitea/gitea", Tag: tag, Commit: commit.SHA, Files: map[string]string{}}, Files: map[string][]byte{}}
for _, item := range tree.Tree {
p := item.Path
_, adapter := AdapterSources[p]
selected := adapter || p == "LICENSE" || p == "public/assets/img/favicon.png" || (strings.HasPrefix(p, "templates/mail/") && strings.HasSuffix(p, ".tmpl")) || (strings.HasPrefix(p, "options/locale/locale_") && strings.HasSuffix(p, ".json"))
if !selected || item.Type != "blob" {
continue
}
b, err := get(raw + commit.SHA + "/" + p)
if err != nil {
return err
}
s.Files[p] = b
s.Lock.Files[p] = Digest(b)
}
if err := s.validate(); err != nil {
return err
}
abs, err := filepath.Abs(dir)
if err != nil {
return err
}
// Never replace a checkout or arbitrary non-snapshot directory.
if filepath.Base(abs) != "upstream" {
return fmt.Errorf("snapshot destination must be named upstream")
}
if entries, err := os.ReadDir(abs); err == nil && len(entries) > 0 {
if _, err := loadFiles(abs); err != nil {
return fmt.Errorf("refusing to replace invalid existing snapshot: %w", err)
}
}
stage, err := os.MkdirTemp(filepath.Dir(abs), ".upstream-stage-")
if err != nil {
return err
}
defer os.RemoveAll(stage)
for p, data := range s.Files {
filename := filepath.Join(stage, filepath.FromSlash(p))
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return err
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return err
}
}
b, err = json.MarshalIndent(s.Lock, "", " ")
if err != nil {
return err
}
b = append(b, '\n')
if err := os.WriteFile(filepath.Join(stage, "lock.json"), b, 0644); err != nil {
return err
}
if _, err := os.Stat(abs); err == nil {
backup, err := os.MkdirTemp(filepath.Dir(abs), ".upstream-backup-")
if err != nil {
return err
}
if err := os.Remove(backup); err != nil {
return err
}
if err := os.Rename(abs, backup); err != nil {
return err
}
if err := os.Rename(stage, abs); err != nil {
_ = os.Rename(backup, abs)
return err
}
if err := os.RemoveAll(backup); err != nil {
return err
}
} else if err := os.Rename(stage, abs); err != nil {
return err
}
fmt.Printf("[PASS] Locked %s (%s): %d templates, %d languages\n", tag, commit.SHA, len(s.Templates), len(s.Locales))
return nil
}