Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/zxh326/kite
go 1.24.3

require (
github.com/blang/semver/v4 v4.0.0
github.com/gin-contrib/gzip v1.2.3
github.com/gin-gonic/gin v1.10.1
github.com/glebarez/sqlite v1.11.0
Expand Down Expand Up @@ -32,7 +33,6 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down
7 changes: 6 additions & 1 deletion pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ var (

CookieExpirationSeconds = 2 * JWTExpirationSeconds // double jwt

DisableGZIP = false
DisableGZIP = false
DisableVersionCheck = false
)

func LoadEnvs() {
Expand Down Expand Up @@ -80,4 +81,8 @@ func LoadEnvs() {
if v := os.Getenv("DISABLE_GZIP"); v == "true" {
DisableGZIP = true
}

if v := os.Getenv("DISABLE_VERSION_CHECK"); v == "true" {
DisableVersionCheck = true
}
}
125 changes: 125 additions & 0 deletions pkg/version/update_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package version

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"

semver "github.com/blang/semver/v4"
"k8s.io/klog/v2"
)

const (
githubLatestReleaseAPI = "https://api.github.com/repos/zxh326/kite/releases/latest"
versionCheckTimeout = 3 * time.Second
versionCacheTTL = time.Hour
)

var (
updateInfoMu sync.Mutex
cachedUpdateResult = updateCheckResult{}
lastUpdateFetch time.Time
)

type githubRelease struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
}

type updateCheckResult struct {
hasNew bool
releaseURL string
}

func checkForUpdate(ctx context.Context, currentVersion string) updateCheckResult {
result := updateCheckResult{}

sanitized := strings.TrimSpace(currentVersion)
if sanitized == "" || strings.EqualFold(sanitized, "dev") {
return result
}

updateInfoMu.Lock()
if time.Since(lastUpdateFetch) < versionCacheTTL {
cached := cachedUpdateResult
updateInfoMu.Unlock()
return cached
}
updateInfoMu.Unlock()

requestCtx, cancel := context.WithTimeout(ctx, versionCheckTimeout)
defer cancel()

req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, githubLatestReleaseAPI, nil)
if err != nil {
klog.Warningf("version check request creation failed: %v", err)
return result
}

req.Header.Set("User-Agent", "kite-version-checker/"+currentVersion)
req.Header.Set("Accept", "application/vnd.github+json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
klog.Warningf("version check request failed: %v", err)
return result
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
klog.Warningf("version check unexpected status: %s", resp.Status)
return result
}

var release githubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
klog.Warningf("version check decode failed: %v", err)
return result
}

latestVersion, err := parseSemver(release.TagName)
if err != nil {
klog.Warningf("latest version parse failed: %v", err)
return result
}

currentSemver, err := parseSemver(sanitized)
if err != nil {
klog.Warningf("current version parse failed: %v", err)
return result
}

if latestVersion.GT(currentSemver) {
result.hasNew = true
result.releaseURL = release.HTMLURL
}

cacheUpdateResult(result)
return result
}

func cacheUpdateResult(result updateCheckResult) {
updateInfoMu.Lock()
cachedUpdateResult = result
lastUpdateFetch = time.Now()
updateInfoMu.Unlock()
}

func parseSemver(version string) (semver.Version, error) {
trimmed := strings.TrimSpace(version)
trimmed = strings.TrimPrefix(trimmed, "v")
if trimmed == "" {
return semver.Version{}, errors.New("empty version")
}

parsed, err := semver.Parse(trimmed)
if err != nil {
return semver.Version{}, fmt.Errorf("invalid semver %q: %w", version, err)
}
return parsed, nil
}
10 changes: 10 additions & 0 deletions pkg/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/zxh326/kite/pkg/common"
)

var (
Expand All @@ -16,6 +17,8 @@ type VersionInfo struct {
Version string `json:"version"`
BuildDate string `json:"buildDate"`
CommitID string `json:"commitId"`
HasNew bool `json:"hasNewVersion"`
Release string `json:"releaseUrl"`
}

func GetVersion(c *gin.Context) {
Expand All @@ -25,5 +28,12 @@ func GetVersion(c *gin.Context) {
CommitID: CommitID,
}

if !common.DisableVersionCheck {
r := checkForUpdate(c.Request.Context(), Version)
versionInfo.HasNew = r.hasNew
if versionInfo.HasNew {
versionInfo.Release = r.releaseURL
}
}
c.JSON(http.StatusOK, versionInfo)
}
23 changes: 22 additions & 1 deletion ui/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ChevronDown } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Link, useLocation } from 'react-router-dom'

import { useVersionInfo } from '@/lib/api'
import {
Sidebar,
SidebarContent,
Expand All @@ -31,6 +32,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const location = useLocation()
const { isMobile, setOpenMobile } = useSidebar()
const { config, isLoading, getIconComponent } = useSidebarConfig()
const { data: versionInfo } = useVersionInfo()

const pinnedItems = useMemo(() => {
if (!config) return []
Expand Down Expand Up @@ -106,7 +108,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
className="data-[slot=sidebar-menu-button]:!p-1.5 hover:bg-accent/50 transition-colors"
>
<Link to="/" onClick={handleMenuItemClick}>
<div className="flex items-center justify-between w-full">
<div className="relative flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<img src={Icon} alt="Kite Logo" className="h-8 w-8" />
<div className="flex flex-col">
Expand All @@ -116,6 +118,25 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<VersionInfo />
</div>
</div>
{versionInfo?.hasNewVersion ? (
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
if (versionInfo?.releaseUrl) {
window.open(versionInfo.releaseUrl, '_blank')
}
}}
className="absolute right-0 top-0 mr-1 mt-1 rounded-sm bg-red-500/10 px-1.5 py-0.5 text-[9px] font-semibold uppercase text-red-500 hover:bg-red-500/20"
title={t(
'A newer Kite version is available',
'A newer Kite version is available'
)}
>
New
</button>
) : null}
</div>
</Link>
</SidebarMenuButton>
Expand Down
2 changes: 2 additions & 0 deletions ui/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,8 @@ export interface VersionInfo {
version: string
buildDate: string
commitId: string
hasNewVersion: boolean
releaseUrl: string
}

export const fetchVersionInfo = (): Promise<VersionInfo> => {
Expand Down