Add automatic update checks.
- A check for new versions on the GitHub releases pages happens once every 24 hours. When a new version is available, a notice is displayed on the admin UI.
This commit is contained in:
parent
f3e80da339
commit
d8a60d1295
2
Makefile
2
Makefile
|
@ -20,7 +20,7 @@ deps:
|
||||||
# Build steps.
|
# Build steps.
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build:
|
build:
|
||||||
go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}'" cmd/*.go
|
go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}'" cmd/*.go
|
||||||
|
|
||||||
.PHONY: build-frontend
|
.PHONY: build-frontend
|
||||||
build-frontend:
|
build-frontend:
|
||||||
|
|
12
cmd/admin.go
12
cmd/admin.go
|
@ -13,11 +13,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type configScript struct {
|
type configScript struct {
|
||||||
RootURL string `json:"rootURL"`
|
RootURL string `json:"rootURL"`
|
||||||
FromEmail string `json:"fromEmail"`
|
FromEmail string `json:"fromEmail"`
|
||||||
Messengers []string `json:"messengers"`
|
Messengers []string `json:"messengers"`
|
||||||
MediaProvider string `json:"mediaProvider"`
|
MediaProvider string `json:"mediaProvider"`
|
||||||
NeedsRestart bool `json:"needsRestart"`
|
NeedsRestart bool `json:"needsRestart"`
|
||||||
|
Update *AppUpdate `json:"update"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetConfigScript returns general configuration as a Javascript
|
// handleGetConfigScript returns general configuration as a Javascript
|
||||||
|
@ -35,6 +36,7 @@ func handleGetConfigScript(c echo.Context) error {
|
||||||
|
|
||||||
app.Lock()
|
app.Lock()
|
||||||
out.NeedsRestart = app.needsRestart
|
out.NeedsRestart = app.needsRestart
|
||||||
|
out.Update = app.update
|
||||||
app.Unlock()
|
app.Unlock()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -34,7 +34,6 @@ var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[
|
||||||
func registerHTTPHandlers(e *echo.Echo) {
|
func registerHTTPHandlers(e *echo.Echo) {
|
||||||
// Group of private handlers with BasicAuth.
|
// Group of private handlers with BasicAuth.
|
||||||
g := e.Group("", middleware.BasicAuth(basicAuth))
|
g := e.Group("", middleware.BasicAuth(basicAuth))
|
||||||
|
|
||||||
g.GET("/", handleIndexPage)
|
g.GET("/", handleIndexPage)
|
||||||
g.GET("/api/health", handleHealthCheck)
|
g.GET("/api/health", handleHealthCheck)
|
||||||
g.GET("/api/config.js", handleGetConfigScript)
|
g.GET("/api/config.js", handleGetConfigScript)
|
||||||
|
|
|
@ -42,6 +42,9 @@ type App struct {
|
||||||
// Global variable that stores the state indicating that a restart is required
|
// Global variable that stores the state indicating that a restart is required
|
||||||
// after a settings update.
|
// after a settings update.
|
||||||
needsRestart bool
|
needsRestart bool
|
||||||
|
|
||||||
|
// Global state that stores data on an available remote update.
|
||||||
|
update *AppUpdate
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +56,8 @@ var (
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
queries *Queries
|
queries *Queries
|
||||||
|
|
||||||
buildString string
|
buildString string
|
||||||
|
versionString string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -137,6 +141,9 @@ func main() {
|
||||||
// Start the app server.
|
// Start the app server.
|
||||||
srv := initHTTPServer(app)
|
srv := initHTTPServer(app)
|
||||||
|
|
||||||
|
// Star the update checker.
|
||||||
|
go checkUpdates(versionString, time.Hour*24, app)
|
||||||
|
|
||||||
// Wait for the reload signal with a callback to gracefully shut down resources.
|
// Wait for the reload signal with a callback to gracefully shut down resources.
|
||||||
// The `wait` channel is passed to awaitReload to wait for the callback to finish
|
// The `wait` channel is passed to awaitReload to wait for the callback to finish
|
||||||
// within N seconds, or do a force reload.
|
// within N seconds, or do a force reload.
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateCheckURL = "https://api.github.com/repos/knadh/listmonk/releases/latest"
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var reSemver = regexp.MustCompile(`-(.*)`)
|
||||||
|
|
||||||
|
// checkUpdates is a blocking function that checks for updates to the app
|
||||||
|
// at the given intervals. On detecting a new update (new semver), it
|
||||||
|
// sets the global update status that renders a prompt on the UI.
|
||||||
|
func checkUpdates(curVersion string, interval time.Duration, app *App) {
|
||||||
|
// Strip -* suffix.
|
||||||
|
curVersion = reSemver.ReplaceAllString(curVersion, "")
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
for ; true; <-ticker.C {
|
||||||
|
resp, err := http.Get(updateCheckURL)
|
||||||
|
if err != nil {
|
||||||
|
app.log.Printf("error checking for remote update: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
app.log.Printf("non 200 response on remote update check: %d", resp.StatusCode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
app.log.Printf("error reading remote update payload: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
var up remoteUpdateResp
|
||||||
|
if err := json.Unmarshal(b, &up); 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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,7 +84,7 @@
|
||||||
|
|
||||||
<!-- body //-->
|
<!-- body //-->
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="global-notices" v-if="serverConfig.needsRestart">
|
<div class="global-notices" v-if="serverConfig.needsRestart || serverConfig.update">
|
||||||
<div v-if="serverConfig.needsRestart" class="notification is-danger">
|
<div v-if="serverConfig.needsRestart" class="notification is-danger">
|
||||||
Settings have changed. Pause all running campaigns and restart the app
|
Settings have changed. Pause all running campaigns and restart the app
|
||||||
—
|
—
|
||||||
|
@ -94,6 +94,10 @@
|
||||||
Restart
|
Restart
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="serverConfig.update" class="notification is-success">
|
||||||
|
A new update ({{ serverConfig.update.version }}) is available.
|
||||||
|
<a :href="serverConfig.update.url" target="_blank">View</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-view :key="$route.fullPath" />
|
<router-view :key="$route.fullPath" />
|
||||||
|
|
|
@ -158,11 +158,17 @@ section {
|
||||||
}
|
}
|
||||||
.notification {
|
.notification {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
|
border-left: 5px solid #eee;
|
||||||
|
|
||||||
&.is-danger {
|
&.is-danger {
|
||||||
background: $white-ter;
|
background: $white-ter;
|
||||||
color: $black;
|
color: $black;
|
||||||
border-left: 5px solid $red;
|
border-left-color: $red;
|
||||||
font-weight: bold;
|
}
|
||||||
|
&.is-success {
|
||||||
|
background: $white-ter;
|
||||||
|
color: $black;
|
||||||
|
border-left-color: $green;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue