Skip to content

Commit

Permalink
Refactor update check.
Browse files Browse the repository at this point in the history
- 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!)
  • Loading branch information
knadh committed Oct 13, 2024
1 parent a8c1778 commit 4eabd96
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 30 deletions.
60 changes: 34 additions & 26 deletions cmd/updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`-(.*)`)
Expand All @@ -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)
Expand All @@ -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)
}
}
1 change: 1 addition & 0 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 17 additions & 3 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/assets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 */
Expand Down

0 comments on commit 4eabd96

Please sign in to comment.