Refactor update check.

- Switch away from GitHub releases API to a statically hosted custom
  JSON message to include richer data.
- Instead of checking 24 hours post-boot, check 15 mins later post boot
  and then every 24 hours.
- Add provision for messages to display on the admin dashboard to
  communicate important / urgent announcements.
  (Fingers crossed, this never has to be used!)
This commit is contained in:
Kailash Nadh 2024-07-06 20:04:16 +05:30
parent a8c17780f9
commit 4eabd967d8
4 changed files with 58 additions and 30 deletions

View file

@ -10,18 +10,25 @@ import (
"golang.org/x/mod/semver"
)
const updateCheckURL = "https://api.github.com/repos/knadh/listmonk/releases/latest"
const updateCheckURL = "https://update.listmonk.app/update.json"
type remoteUpdateResp struct {
Version string `json:"tag_name"`
URL string `json:"html_url"`
}
// AppUpdate contains information of a new update available to the app that
// is sent to the frontend.
type AppUpdate struct {
Version string `json:"version"`
URL string `json:"url"`
Update struct {
ReleaseVersion string `json:"release_version"`
ReleaseDate string `json:"release_date"`
URL string `json:"url"`
Description string `json:"description"`
// This is computed and set locally based on the local version.
IsNew bool `json:"is_new"`
} `json:"update"`
Messages []struct {
Date string `json:"date"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
Priority string `json:"priority"`
} `json:"messages"`
}
var reSemver = regexp.MustCompile(`-(.*)`)
@ -32,11 +39,12 @@ var reSemver = regexp.MustCompile(`-(.*)`)
func checkUpdates(curVersion string, interval time.Duration, app *App) {
// Strip -* suffix.
curVersion = reSemver.ReplaceAllString(curVersion, "")
time.Sleep(time.Second * 1)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
// Give a 15 minute buffer after app start in case the admin wants to disable
// update checks entirely and not make a request to upstream.
time.Sleep(time.Minute * 1)
for {
resp, err := http.Get(updateCheckURL)
if err != nil {
app.log.Printf("error checking for remote update: %v", err)
@ -55,25 +63,25 @@ func checkUpdates(curVersion string, interval time.Duration, app *App) {
}
resp.Body.Close()
var up remoteUpdateResp
if err := json.Unmarshal(b, &up); err != nil {
var out AppUpdate
if err := json.Unmarshal(b, &out); err != nil {
app.log.Printf("error unmarshalling remote update payload: %v", err)
continue
}
// There is an update. Set it on the global app state.
if semver.IsValid(up.Version) {
v := reSemver.ReplaceAllString(up.Version, "")
if semver.IsValid(out.Update.ReleaseVersion) {
v := reSemver.ReplaceAllString(out.Update.ReleaseVersion, "")
if semver.Compare(v, curVersion) > 0 {
app.Lock()
app.update = &AppUpdate{
Version: up.Version,
URL: up.URL,
}
app.Unlock()
app.log.Printf("new update %s found", up.Version)
out.Update.IsNew = true
app.log.Printf("new update %s found", out.Update.ReleaseVersion)
}
}
app.Lock()
app.update = &out
app.Unlock()
time.Sleep(interval)
}
}

View file

@ -21,6 +21,7 @@ module.exports = {
'vue/max-attributes-per-line': 'off',
'vue/html-indent': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/max-len': ['error', {
code: 200,
template: 200,

View file

@ -58,10 +58,24 @@
{{ $t('settings.restart') }}
</b-button>
</div>
<div v-if="serverConfig.update" class="notification is-success">
{{ $t('settings.updateAvailable', { version: serverConfig.update.version }) }}
<a :href="serverConfig.update.url" target="_blank" rel="noopener noreferer">View</a>
<div v-if="serverConfig.update.update.is_new" class="notification is-success">
{{ $t('settings.updateAvailable', {
version: `${serverConfig.update.update.release_version}
(${$utils.getDate(serverConfig.update.update.release_date).format('DD MMM YY')})`,
}) }}
<a :href="serverConfig.update.update.url" target="_blank" rel="noopener noreferer">View</a>
</div>
<template v-if="serverConfig.update.messages && serverConfig.update.messages.length > 0">
<div v-for="m in serverConfig.update.messages" class="notification"
:class="{ [m.priority === 'high' ? 'is-danger' : 'is-info']: true }" :key="m.title">
<h3 class="is-size-5" v-if="m.title"><strong>{{ m.title }}</strong></h3>
<p v-if="m.description">{{ m.description }}</p>
<a v-if="m.url" :href="m.url" target="_blank" rel="noopener noreferer">View</a>
</div>
</template>
<div v-if="serverConfig.has_legacy_user" class="notification is-danger">
<b-icon icon="warning-empty" />
Remove the <code>admin_username</code> and <code>admin_password</code> fields from the TOML

View file

@ -252,7 +252,7 @@ body.is-noscroll {
}
.notification {
padding: 10px 15px;
border-left: 5px solid #eee;
border-left: 10px solid #eee;
&.is-danger {
background: $white-ter;
@ -264,6 +264,11 @@ body.is-noscroll {
color: $black;
border-left-color: $green;
}
&.is-info {
background: $white-ter;
border-left-color: $primary;
color: $grey-dark;
}
}
/* WYSIWYG / HTML code editor */