260 lines
6.9 KiB
Go
260 lines
6.9 KiB
Go
package githubreleases
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/Eriyc/rules_wails/pkg/wails3kit/updates"
|
|
)
|
|
|
|
const (
|
|
defaultAssetTemplate = "{{ .ProductID }}_{{ .Version }}_{{ .OS }}_{{ .Arch }}.zip"
|
|
defaultChecksumAssetTemplate = "{{ .AssetName }}.sha256"
|
|
)
|
|
|
|
type Config struct {
|
|
Owner string
|
|
Repo string
|
|
HTTPClient *http.Client
|
|
PrepareRequest func(*http.Request) error
|
|
AssetNameTemplate string
|
|
ChecksumAssetNameTemplate string
|
|
}
|
|
|
|
type Provider struct {
|
|
config Config
|
|
client *http.Client
|
|
assetNameTemplate *template.Template
|
|
checksumNameTemplate *template.Template
|
|
baseURL string
|
|
}
|
|
|
|
func New(cfg Config) (*Provider, error) {
|
|
if cfg.Owner == "" || cfg.Repo == "" {
|
|
return nil, updates.ErrInvalidConfig
|
|
}
|
|
client := cfg.HTTPClient
|
|
if client == nil {
|
|
client = http.DefaultClient
|
|
}
|
|
assetTemplate := cfg.AssetNameTemplate
|
|
if assetTemplate == "" {
|
|
assetTemplate = defaultAssetTemplate
|
|
}
|
|
checksumTemplate := cfg.ChecksumAssetNameTemplate
|
|
if checksumTemplate == "" {
|
|
checksumTemplate = defaultChecksumAssetTemplate
|
|
}
|
|
|
|
assetNameTemplate, err := template.New("asset").Parse(assetTemplate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
checksumNameTemplate, err := template.New("checksum").Parse(checksumTemplate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Provider{
|
|
config: cfg,
|
|
client: client,
|
|
assetNameTemplate: assetNameTemplate,
|
|
checksumNameTemplate: checksumNameTemplate,
|
|
baseURL: "https://api.github.com",
|
|
}, nil
|
|
}
|
|
|
|
type githubRelease struct {
|
|
TagName string `json:"tag_name"`
|
|
Body string `json:"body"`
|
|
Prerelease bool `json:"prerelease"`
|
|
PublishedAt time.Time `json:"published_at"`
|
|
Assets []githubAsset `json:"assets"`
|
|
}
|
|
|
|
type githubAsset struct {
|
|
Name string `json:"name"`
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
}
|
|
|
|
func (provider *Provider) Resolve(ctx context.Context, req updates.ResolveRequest) (*updates.Release, error) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, provider.releaseURL(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if provider.config.PrepareRequest != nil {
|
|
if err := provider.config.PrepareRequest(request); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
response, err := provider.client.Do(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("unexpected github status: %s", response.Status)
|
|
}
|
|
|
|
var releases []githubRelease
|
|
if err := json.NewDecoder(response.Body).Decode(&releases); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var best *updates.Release
|
|
for _, release := range releases {
|
|
version := strings.TrimPrefix(release.TagName, "v")
|
|
channel := mapChannel(release)
|
|
if channel != req.App.Channel {
|
|
continue
|
|
}
|
|
comparison, err := updates.CompareVersions(version, req.App.CurrentVersion)
|
|
if err != nil || comparison <= 0 {
|
|
continue
|
|
}
|
|
|
|
assetName, err := provider.renderAssetName(req.App, version, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
checksumName, err := provider.renderChecksumName(req.App, version, assetName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
artifactURL := ""
|
|
checksumURL := ""
|
|
for _, asset := range release.Assets {
|
|
if asset.Name == assetName {
|
|
artifactURL = asset.BrowserDownloadURL
|
|
}
|
|
if asset.Name == checksumName {
|
|
checksumURL = asset.BrowserDownloadURL
|
|
}
|
|
}
|
|
if artifactURL == "" || checksumURL == "" {
|
|
continue
|
|
}
|
|
|
|
sha256Value, err := provider.readChecksum(ctx, checksumURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
candidate := &updates.Release{
|
|
ID: version,
|
|
Version: version,
|
|
Channel: channel,
|
|
NotesMarkdown: release.Body,
|
|
PublishedAt: release.PublishedAt,
|
|
Artifact: updates.Artifact{
|
|
Kind: updates.ArtifactKindBundleArchive,
|
|
Format: updates.ArtifactFormatZip,
|
|
URL: artifactURL,
|
|
SHA256: sha256Value,
|
|
},
|
|
}
|
|
if best == nil {
|
|
best = candidate
|
|
continue
|
|
}
|
|
better, err := updates.CompareVersions(candidate.Version, best.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if better > 0 {
|
|
best = candidate
|
|
}
|
|
}
|
|
|
|
return best, nil
|
|
}
|
|
|
|
func (provider *Provider) OpenArtifact(ctx context.Context, release updates.Release) (io.ReadCloser, error) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, release.Artifact.URL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if provider.config.PrepareRequest != nil {
|
|
if err := provider.config.PrepareRequest(request); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
response, err := provider.client.Do(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
defer response.Body.Close()
|
|
return nil, fmt.Errorf("unexpected artifact status: %s", response.Status)
|
|
}
|
|
return response.Body, nil
|
|
}
|
|
|
|
func (provider *Provider) readChecksum(ctx context.Context, url string) (string, error) {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if provider.config.PrepareRequest != nil {
|
|
if err := provider.config.PrepareRequest(request); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
response, err := provider.client.Do(request)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return "", fmt.Errorf("unexpected checksum status: %s", response.Status)
|
|
}
|
|
bytes, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.Fields(string(bytes))[0], nil
|
|
}
|
|
|
|
func (provider *Provider) releaseURL() string {
|
|
return fmt.Sprintf("%s/repos/%s/%s/releases", provider.baseURL, provider.config.Owner, provider.config.Repo)
|
|
}
|
|
|
|
func (provider *Provider) renderAssetName(app updates.AppDescriptor, version string, assetName string) (string, error) {
|
|
return provider.executeTemplate(provider.assetNameTemplate, app, version, assetName)
|
|
}
|
|
|
|
func (provider *Provider) renderChecksumName(app updates.AppDescriptor, version string, assetName string) (string, error) {
|
|
return provider.executeTemplate(provider.checksumNameTemplate, app, version, assetName)
|
|
}
|
|
|
|
func (provider *Provider) executeTemplate(templateValue *template.Template, app updates.AppDescriptor, version string, assetName string) (string, error) {
|
|
var builder strings.Builder
|
|
err := templateValue.Execute(&builder, map[string]string{
|
|
"ProductID": app.ProductID,
|
|
"Version": version,
|
|
"OS": app.OS,
|
|
"Arch": app.Arch,
|
|
"AssetName": assetName,
|
|
})
|
|
return builder.String(), err
|
|
}
|
|
|
|
func mapChannel(release githubRelease) updates.Channel {
|
|
if !release.Prerelease {
|
|
return updates.ChannelStable
|
|
}
|
|
if strings.Contains(strings.ToLower(release.TagName), "alpha") {
|
|
return updates.ChannelAlpha
|
|
}
|
|
return updates.ChannelBeta
|
|
}
|