Add additional latest version info (#3357)

This commit is contained in:
DingDongSoLong4
2023-02-08 06:02:23 +02:00
committed by GitHub
parent cb808c7be2
commit 901a7e59ec
8 changed files with 206 additions and 167 deletions

View File

@@ -67,7 +67,9 @@ query Version {
query LatestVersion { query LatestVersion {
latestversion { latestversion {
version
shorthash shorthash
release_date
url url
} }
} }

View File

@@ -155,7 +155,7 @@ type Query {
version: Version! version: Version!
# LatestVersion # LatestVersion
latestversion: ShortVersion! latestversion: LatestVersion!
} }
type Mutation { type Mutation {

View File

@@ -4,7 +4,9 @@ type Version {
build_time: String! build_time: String!
} }
type ShortVersion { type LatestVersion {
version: String!
shorthash: String! shorthash: String!
release_date: String!
url: String! url: String!
} }

View File

@@ -20,12 +20,7 @@ import (
const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases" const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases"
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags" const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
const apiAcceptHeader string = "application/vnd.github.v3+json" const apiAcceptHeader string = "application/vnd.github.v3+json"
const developmentTag string = "latest_develop" const defaultSHLength int = 8 // default length of SHA short hash returned by <git rev-parse --short HEAD>
const defaultSHLength int = 7 // default length of SHA short hash returned by <git rev-parse --short HEAD>
// ErrNoVersion indicates that no version information has been embedded in the
// stash binary
var ErrNoVersion = errors.New("no stash version")
var stashReleases = func() map[string]string { var stashReleases = func() map[string]string {
return map[string]string{ return map[string]string{
@@ -108,6 +103,14 @@ type githubTagResponse struct {
Node_id string Node_id string
} }
type LatestRelease struct {
Version string
Hash string
ShortHash string
Date string
Url string
}
func makeGithubRequest(ctx context.Context, url string, output interface{}) error { func makeGithubRequest(ctx context.Context, url string, output interface{}) error {
transport := &http.Transport{Proxy: http.ProxyFromEnvironment} transport := &http.Transport{Proxy: http.ProxyFromEnvironment}
@@ -148,14 +151,16 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro
return nil return nil
} }
// GetLatestVersion gets latest version (git commit hash) from github API // GetLatestRelease gets latest release information from github API
// If running a build from the "master" branch, then the latest full release // If running a build from the "master" branch, then the latest full release
// is used, otherwise it uses the release that is tagged with "latest_develop" // is used, otherwise it uses the release that is tagged with "latest_develop"
// which is the latest pre-release build. // which is the latest pre-release build.
func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string, latestRelease string, err error) { func GetLatestRelease(ctx context.Context) (*LatestRelease, error) {
arch := runtime.GOARCH
arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features // armv6 doesn't support any of these features
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4
if arch == "arm" && isARMv7 { if arch == "arm" && isARMv7 {
arch = "armv7" arch = "armv7"
} }
@@ -163,125 +168,100 @@ func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string
platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch) platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch)
wantedRelease := stashReleases()[platform] wantedRelease := stashReleases()[platform]
version, _, _ := GetVersion() var release githubReleasesResponse
if version == "" { if IsDevelop() {
return "", "", ErrNoVersion // get the latest release, prerelease or not
} releases := []githubReleasesResponse{}
err := makeGithubRequest(ctx, apiReleases+"?per_page=1", &releases)
// if the version is suffixed with -x-xxxx, then we are running a development build if err != nil {
usePreRelease := false return nil, err
re := regexp.MustCompile(`-\d+-g\w+$`) }
if re.MatchString(version) { release = releases[0]
usePreRelease = true
}
url := apiReleases
if !usePreRelease {
// just get the latest full release
url += "/latest"
} else { } else {
// get the release tagged with the development tag // just get the latest full release
url += "/tags/" + developmentTag err := makeGithubRequest(ctx, apiReleases+"/latest", &release)
if err != nil {
return nil, err
}
} }
release := githubReleasesResponse{} version := release.Name
err = makeGithubRequest(ctx, url, &release) if release.Prerelease {
// find version in prerelease name
re := regexp.MustCompile(`v[\w-\.]+-\d+-g[0-9a-f]+`)
if match := re.FindString(version); match != "" {
version = match
}
}
latestHash, err := getReleaseHash(ctx, release.Tag_name)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
if release.Prerelease == usePreRelease { var releaseDate string
latestVersion = getReleaseHash(ctx, release, shortHash, usePreRelease) if publishedAt, err := time.Parse(time.RFC3339, release.Published_at); err == nil {
releaseDate = publishedAt.Format("2006-01-02")
}
if wantedRelease != "" { var releaseUrl string
for _, asset := range release.Assets { if wantedRelease != "" {
if asset.Name == wantedRelease { for _, asset := range release.Assets {
latestRelease = asset.Browser_download_url if asset.Name == wantedRelease {
break releaseUrl = asset.Browser_download_url
} break
} }
} }
} }
if latestVersion == "" { _, githash, _ := GetVersion()
return "", "", fmt.Errorf("no version found for \"%s\"", version) shLength := len(githash)
if shLength == 0 {
shLength = defaultSHLength
} }
return latestVersion, latestRelease, nil
return &LatestRelease{
Version: version,
Hash: latestHash,
ShortHash: latestHash[:shLength],
Date: releaseDate,
Url: releaseUrl,
}, nil
} }
func getReleaseHash(ctx context.Context, release githubReleasesResponse, shortHash bool, usePreRelease bool) string { func getReleaseHash(ctx context.Context, tagName string) (string, error) {
shaLength := len(release.Target_commitish)
// the /latest API call doesn't return the hash in target_commitish
// also add sanity check in case Target_commitish is not 40 characters
if !usePreRelease || shaLength != 40 {
return getShaFromTags(ctx, shortHash, release.Tag_name)
}
if shortHash {
last := defaultSHLength // default length of git short hash
_, gitShort, _ := GetVersion() // retrieve it to check actual length
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return release.Target_commitish[0:last]
}
return release.Target_commitish
}
func printLatestVersion(ctx context.Context) {
_, githash, _ = GetVersion()
latest, _, err := GetLatestVersion(ctx, true)
if err != nil {
logger.Errorf("Couldn't find latest version: %s", err)
} else {
if githash == latest {
logger.Infof("Version: (%s) is already the latest released.", latest)
} else {
logger.Infof("New version: (%s) available.", latest)
}
}
}
// get sha from the github api tags endpoint
// returns the sha1 hash/shorthash or "" if something's wrong
func getShaFromTags(ctx context.Context, shortHash bool, name string) string {
url := apiTags url := apiTags
tags := []githubTagResponse{} tags := []githubTagResponse{}
err := makeGithubRequest(ctx, url, &tags) err := makeGithubRequest(ctx, url, &tags)
if err != nil { if err != nil {
// If the context is canceled, we don't want to log this as an error return "", err
// in the path. The function here just gives up and returns "" if
// something goes wrong. Hence, log the error at the info-level so
// it's still present, but don't treat this as an error.
if errors.Is(err, context.Canceled) {
logger.Infof("aborting sha request due to context cancellation")
} else {
logger.Errorf("Github Tags Api: %v", err)
}
return ""
} }
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length
for _, tag := range tags { for _, tag := range tags {
if tag.Name == name { if tag.Name == tagName {
shaLength := len(tag.Commit.Sha) if len(tag.Commit.Sha) != 40 {
if shaLength != 40 { return "", errors.New("invalid Github API response")
return ""
} }
if shortHash { return tag.Commit.Sha, nil
last := defaultSHLength // default length of git short hash
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return tag.Commit.Sha[0:last]
}
return tag.Commit.Sha
} }
} }
return "" return "", errors.New("invalid Github API response")
}
func printLatestVersion(ctx context.Context) {
latestRelease, err := GetLatestRelease(ctx)
if err != nil {
logger.Errorf("Couldn't retrieve latest version: %v", err)
} else {
_, githash, _ = GetVersion()
switch {
case githash == "":
logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
case githash == latestRelease.ShortHash:
logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash)
default:
logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
}
}
} }

View File

@@ -184,19 +184,22 @@ func (r *queryResolver) Version(ctx context.Context) (*Version, error) {
}, nil }, nil
} }
// Latestversion returns the latest git shorthash commit. func (r *queryResolver) Latestversion(ctx context.Context) (*LatestVersion, error) {
func (r *queryResolver) Latestversion(ctx context.Context) (*ShortVersion, error) { latestRelease, err := GetLatestRelease(ctx)
ver, url, err := GetLatestVersion(ctx, true) if err != nil {
if err == nil { if !errors.Is(err, context.Canceled) {
logger.Infof("Retrieved latest hash: %s", ver) logger.Errorf("Error while retrieving latest version: %v", err)
} else { }
logger.Errorf("Error while retrieving latest hash: %s", err) return nil, err
} }
logger.Infof("Retrieved latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
return &ShortVersion{ return &LatestVersion{
Shorthash: ver, Version: latestRelease.Version,
URL: url, Shorthash: latestRelease.ShortHash,
}, err ReleaseDate: latestRelease.Date,
URL: latestRelease.Url,
}, nil
} }
// Get scene marker tags which show up under the video. // Get scene marker tags which show up under the video.

View File

@@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"regexp"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"strings" "strings"
@@ -392,22 +393,48 @@ func javascriptHandler(c *config.Instance, pluginCache *plugin.Cache) func(w htt
} }
func printVersion() { func printVersion() {
versionString := githash var versionString string
switch {
case version != "":
if githash != "" && !IsDevelop() {
versionString = version + " (" + githash + ")"
} else {
versionString = version
}
case githash != "":
versionString = githash
default:
versionString = "unknown"
}
if config.IsOfficialBuild() { if config.IsOfficialBuild() {
versionString += " - Official Build" versionString += " - Official Build"
} else { } else {
versionString += " - Unofficial Build" versionString += " - Unofficial Build"
} }
if version != "" { if buildstamp != "" {
versionString = version + " (" + versionString + ")" versionString += " - " + buildstamp
} }
fmt.Printf("stash version: %s - %s\n", versionString, buildstamp) logger.Infof("stash version: %s\n", versionString)
} }
func GetVersion() (string, string, string) { func GetVersion() (string, string, string) {
return version, githash, buildstamp return version, githash, buildstamp
} }
func IsDevelop() bool {
if githash == "" {
return false
}
// if the version is suffixed with -x-xxxx, then we are running a development build
develop := false
re := regexp.MustCompile(`-\d+-g\w+$`)
if re.MatchString(version) {
develop = true
}
return develop
}
func makeTLSConfig(c *config.Instance) (*tls.Config, error) { func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
c.InitTLS() c.InitTLS()
certFile, keyFile := c.GetTLSFiles() certFile, keyFile := c.GetTLSFiles()
@@ -428,12 +455,12 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
cert, err := os.ReadFile(certFile) cert, err := os.ReadFile(certFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading SSL certificate file %s: %s", certFile, err.Error()) return nil, fmt.Errorf("error reading SSL certificate file %s: %v", certFile, err)
} }
key, err := os.ReadFile(keyFile) key, err := os.ReadFile(keyFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading SSL key file %s: %s", keyFile, err.Error()) return nil, fmt.Errorf("error reading SSL key file %s: %v", keyFile, err)
} }
certs := make([]tls.Certificate, 1) certs := make([]tls.Certificate, 1)

View File

@@ -2,7 +2,7 @@ import React from "react";
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useLatestVersion } from "src/core/StashService"; import { useLatestVersion } from "src/core/StashService";
import { ConstantSetting, Setting, SettingGroup } from "./Inputs"; import { ConstantSetting, SettingGroup } from "./Inputs";
import { SettingSection } from "./SettingSection"; import { SettingSection } from "./SettingSection";
export const SettingsAboutPanel: React.FC = () => { export const SettingsAboutPanel: React.FC = () => {
@@ -20,7 +20,69 @@ export const SettingsAboutPanel: React.FC = () => {
networkStatus, networkStatus,
} = useLatestVersion(); } = useLatestVersion();
const hasNew = dataLatest && gitHash !== dataLatest.latestversion.shorthash; function renderLatestVersion() {
if (errorLatest) {
return (
<SettingGroup
settingProps={{
heading: errorLatest.message,
}}
/>
);
} else if (!dataLatest || loadingLatest || networkStatus === 4) {
return (
<SettingGroup
settingProps={{
headingID: "loading.generic",
}}
/>
);
} else {
let heading = dataLatest.latestversion.version;
const hashString = dataLatest.latestversion.shorthash;
if (gitHash !== hashString) {
heading +=
" " +
intl.formatMessage({
id: "config.about.new_version_notice",
});
}
return (
<SettingGroup
settingProps={{
heading,
}}
>
<div className="setting">
<div>
<h3>
{intl.formatMessage({
id: "config.about.build_hash",
})}
</h3>
<div className="value">{hashString}</div>
</div>
<div>
<a href={dataLatest.latestversion.url}>
<Button>
{intl.formatMessage({ id: "actions.download" })}
</Button>
</a>
<Button onClick={() => refetch()}>
{intl.formatMessage({
id: "config.about.check_for_new_version",
})}
</Button>
</div>
</div>
<ConstantSetting
headingID="config.about.release_date"
value={dataLatest.latestversion.release_date}
/>
</SettingGroup>
);
}
}
return ( return (
<> <>
@@ -39,48 +101,10 @@ export const SettingsAboutPanel: React.FC = () => {
value={buildTime} value={buildTime}
/> />
</SettingGroup> </SettingGroup>
</SettingSection>
<SettingGroup <SettingSection headingID="config.about.latest_version">
settingProps={{ {renderLatestVersion()}
headingID: "config.about.latest_version",
}}
>
{errorLatest ? (
<Setting heading={errorLatest.message} />
) : !dataLatest || loadingLatest || networkStatus === 4 ? (
<Setting headingID="loading.generic" />
) : (
<div className="setting">
<div>
<h3>
{intl.formatMessage({
id: "config.about.latest_version_build_hash",
})}
</h3>
<div className="value">
{dataLatest.latestversion.shorthash}{" "}
{hasNew
? intl.formatMessage({
id: "config.about.new_version_notice",
})
: undefined}
</div>
</div>
<div>
<a href={dataLatest.latestversion.url}>
<Button>
{intl.formatMessage({ id: "actions.download" })}
</Button>
</a>
<Button onClick={() => refetch()}>
{intl.formatMessage({
id: "config.about.check_for_new_version",
})}
</Button>
</div>
</div>
)}
</SettingGroup>
</SettingSection> </SettingSection>
<SettingSection headingID="config.categories.about"> <SettingSection headingID="config.categories.about">

View File

@@ -185,6 +185,7 @@
"latest_version": "Latest Version", "latest_version": "Latest Version",
"latest_version_build_hash": "Latest Version Build Hash:", "latest_version_build_hash": "Latest Version Build Hash:",
"new_version_notice": "[NEW]", "new_version_notice": "[NEW]",
"release_date": "Release date:",
"stash_discord": "Join our {url} channel", "stash_discord": "Join our {url} channel",
"stash_home": "Stash home at {url}", "stash_home": "Stash home at {url}",
"stash_open_collective": "Support us through {url}", "stash_open_collective": "Support us through {url}",