Loading cmd/felis/update.go +85 −0 Changes for cmd/felis/update.go: 85 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -165,6 +165,7 @@ func cmdUpdate(args []string, stdout, stderr io.Writer) int { force := fs.Bool("force", false, "print the apply command for a selected component even when it is already up to date") velocityJar := fs.String("velocity-jar", updater.DefaultVelocityJarPath, "path to the installed Velocity jar to read the current version from") cfgPath := fs.String("config", "/etc/felis/felis.toml", "path to felis.toml, read for the maintenance window the panel stores") record := fs.Bool("record", false, "also store this check for the panel's Updates page (felis-update-check.timer runs it daily)") if err := fs.Parse(args); err != nil { return 2 } Loading Loading @@ -209,9 +210,93 @@ func cmdUpdate(args []string, stdout, stderr io.Writer) int { } fmt.Fprint(stdout, renderApplyGuidance(res, selected, *force)) } if *record { // A fresh context: the discovery pass may have spent most of updateTimeout. rctx, rcancel := context.WithTimeout(context.Background(), updateWindowTimeout) defer rcancel() if err := recordUpdateStatus(rctx, *cfgPath, buildStatusReport(res, src.Notes(), resolvedVersion(), now)); err != nil { fmt.Fprintf(stderr, "felis update: record the check for the panel: %v\n", err) return 1 } fmt.Fprint(stdout, "Recorded this check for the panel's Updates page.\n") } return 0 } // buildStatusReport turns one run into the record the panel shows: every planned // component in plan order, then each component whose installed version could not // be read, by name. A component the feed could not answer for is StateUnknown with // the reason, never StateCurrent: the panel must not call a component current when // nobody could check. func buildStatusReport(res updater.Result, notes map[string]string, felis string, now time.Time) updates.StatusReport { selectorOf := map[string]string{} for _, t := range updateTargets { if t.component != "" && selectorOf[t.component] == "" { selectorOf[t.component] = t.selector } } rep := updates.StatusReport{CheckedAt: now.UTC(), Felis: felis, Components: []updates.ComponentStatus{}} for _, a := range res.RunResult.Plan { cs := updates.ComponentStatus{ Name: a.Component, Current: a.Current.String(), Selector: selectorOf[a.Component], Note: notes[a.Component], } switch { case a.Kind == updates.ActionPinned: cs.State = updates.StatePinned case a.Kind == updates.ActionNotify || a.Kind == updates.ActionApply: cs.State = updates.StateAvailable cs.Latest = a.Latest.String() case a.LatestKnown: cs.State = updates.StateCurrent default: cs.State = updates.StateUnknown if err := res.RunResult.SourceErrors[a.Component]; err != nil { cs.Error = err.Error() } } rep.Components = append(rep.Components, cs) } names := make([]string, 0, len(res.GatherErrors)) for name := range res.GatherErrors { names = append(names, name) } sort.Strings(names) for _, name := range names { rep.Components = append(rep.Components, updates.ComponentStatus{ Name: name, State: updates.StateUnreadable, Selector: selectorOf[name], Note: notes[name], Error: res.GatherErrors[name].Error(), }) } return rep } // recordUpdateStatus upserts rep into platform_settings[updates.StatusKey], the // row the API serves to the panel's Updates page. func recordUpdateStatus(ctx context.Context, cfgPath string, rep updates.StatusReport) error { cfg, err := config.Load(cfgPath) if err != nil { return err } v, err := json.Marshal(rep) if err != nil { return err } conn, err := pgx.Connect(ctx, cfg.Database.URL) if err != nil { return err } defer conn.Close(context.Background()) _, err = conn.Exec(ctx, `INSERT INTO platform_settings (key, value) VALUES ($1, $2::jsonb) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, updates.StatusKey, string(v)) return err } // renderUpdateReport renders the component status table. With no selectors it shows // every tracked component; with selectors it shows only the components those // selectors name, so `felis update --velocity` is a focused answer rather than the Loading cmd/felis/update_test.go +52 −0 Changes for cmd/felis/update_test.go: 52 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "encoding/json" "errors" "strings" "testing" "time" "felis.lolicon.best/internal/updater" "felis.lolicon.best/internal/updates" Loading Loading @@ -279,3 +281,53 @@ func TestInstallerRefNamesATag(t *testing.T) { t.Errorf("no felis-api row: installerRef = %q, want main", got) } } func mustVersion(t *testing.T, s string) updates.Version { t.Helper() v, err := updates.Parse(s) if err != nil { t.Fatalf("parse %q: %v", s, err) } return v } // The record the panel shows keeps every component with a state that cannot be // mistaken: a feed failure is "unknown" with its reason, an unreadable install is // listed after the plan, and each row carries the selector that prints its apply. func TestBuildStatusReport(t *testing.T) { res := planResult([]updates.Action{ {Component: "felis-api", Current: mustVersion(t, "v0.4.0"), Latest: mustVersion(t, "v0.5.0"), LatestKnown: true, Kind: updates.ActionNotify}, {Component: "velocity", Current: mustVersion(t, "3.4.0"), Latest: mustVersion(t, "3.4.0"), LatestKnown: true, Kind: updates.ActionNone}, {Component: "k3s", Current: mustVersion(t, "v1.36.2+k3s1"), Kind: updates.ActionNone}, {Component: "cloudflared", Current: mustVersion(t, "2026.6.1"), Latest: mustVersion(t, "2026.9.0"), LatestKnown: true, Kind: updates.ActionApply}, {Component: "mc-lobby", Current: mustVersion(t, "1.21.4"), Kind: updates.ActionPinned}, }) res.RunResult.SourceErrors["k3s"] = errors.New("github: HTTP 403") res.GatherErrors["postgresql"] = errors.New("psql: not found") res.GatherErrors["jre"] = errors.New("release file missing") notes := map[string]string{"postgresql": "PostgreSQL 13 is past its end of life", "velocity": "pinned minor 3.4"} now := time.Date(2026, 9, 25, 3, 4, 5, 0, time.FixedZone("CST", 8*3600)) b, err := json.Marshal(buildStatusReport(res, notes, "v0.4.0", now)) if err != nil { t.Fatal(err) } want := `{"checked_at":"2026-09-24T19:04:05Z","felis":"v0.4.0","components":[` + `{"name":"felis-api","current":"v0.4.0","latest":"v0.5.0","state":"available","selector":"panel"},` + `{"name":"velocity","current":"3.4.0","state":"current","selector":"velocity","note":"pinned minor 3.4"},` + `{"name":"k3s","current":"v1.36.2+k3s1","state":"unknown","selector":"k3s","error":"github: HTTP 403"},` + `{"name":"cloudflared","current":"2026.6.1","latest":"2026.9.0","state":"available","selector":"cloudflared"},` + `{"name":"mc-lobby","current":"1.21.4","state":"pinned"},` + `{"name":"jre","state":"unreadable","selector":"jre","error":"release file missing"},` + `{"name":"postgresql","state":"unreadable","selector":"postgres","note":"PostgreSQL 13 is past its end of life","error":"psql: not found"}]}` if string(b) != want { t.Errorf("status report =\n%s\nwant\n%s", b, want) } // Nothing tracked still records an empty list, so the panel can tell "checked, // nothing to show" from a report that never arrived. b, _ = json.Marshal(buildStatusReport(planResult(nil), nil, "v0.4.0", now)) if !strings.Contains(string(b), `"components":[]`) { t.Errorf("an empty check = %s, want an empty components list", b) } } deploy/bootstrap.sh +44 −0 Changes for deploy/bootstrap.sh: 44 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -346,6 +346,8 @@ DB_BACKUP_SERVICE="/etc/systemd/system/felis-db-backup.service" DB_BACKUP_TIMER="/etc/systemd/system/felis-db-backup.timer" WATCHDOG_SERVICE="/etc/systemd/system/felis-watchdog.service" WATCHDOG_TIMER="/etc/systemd/system/felis-watchdog.timer" UPDATE_CHECK_SERVICE="/etc/systemd/system/felis-update-check.service" UPDATE_CHECK_TIMER="/etc/systemd/system/felis-update-check.timer" WATCHDOG_STATE="/var/lib/felis/watchdog/state.json" OFFSITE_ENV="${STATE_DIR}/offsite.env" OFFSITE_SERVICE="/etc/systemd/system/felis-offsite.service" Loading Loading @@ -3262,6 +3264,46 @@ summary_offsite() { # memory, and mails the owners (their verified addresses, over the [smtp] relay) what # has stayed wrong long enough to matter. It runs on the host so a k3s that is down is # still reported. The first run happens now, so a broken unit shows up in this install. # The daily version check. Felis applies no update on its own; `felis update --record` # compares what this host runs with the newest upstream releases and stores the result, # which the panel's Updates page shows with the command that applies each update. It runs # on the host because that is where the installed versions are readable. The first check # runs in the background: it waits on the release feeds, and nothing in the install # depends on it. install_update_check_timer() { cat > "$UPDATE_CHECK_SERVICE" <<EOF [Unit] Description=Felis component version check (felis update --record) After=network-online.target postgresql.service k3s.service Wants=network-online.target [Service] Type=oneshot ExecStart=${HOST_BIN} update --record -config ${STATE_DIR}/felis.host.toml TimeoutStartSec=5min Nice=10 PrivateTmp=yes NoNewPrivileges=yes ProtectSystem=full EOF cat > "$UPDATE_CHECK_TIMER" <<EOF [Unit] Description=Daily Felis component version check [Timer] OnCalendar=*-*-* 05:30:00 RandomizedDelaySec=30min Persistent=true [Install] WantedBy=timers.target EOF systemctl daemon-reload systemctl enable --now felis-update-check.timer systemctl start --no-block felis-update-check.service ok "version check: daily; the panel's Updates page shows what has a newer release (journalctl -u felis-update-check)" } install_watchdog_timer() { local disks="/,/var/lib/rancher/k3s,/var/lib/postgresql,/var/lib/felis" path for path in "$FELIS_WORLDS_HOST_PATH" "$FELIS_ARCHIVE_LOCAL_PATH" "$FELIS_DB_BACKUP_DIR"; do Loading Loading @@ -4216,6 +4258,8 @@ main() { install_db_backup_timer # After the backup timer: its first bundle is part of the first copy. install_offsite_timer # After install_velocity: the check reads the installed proxy jar's version. install_update_check_timer # Last: its first run should see the platform as this install leaves it. install_watchdog_timer mark_bootstrap_done Loading deploy/bootstrap_test.sh +28 −0 Changes for deploy/bootstrap_test.sh: 28 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1530,6 +1530,34 @@ expect "a failed first backup shows its log" "JOURNAL: pg_dump: connection refus expect "a failed first backup is a loud warning" "WARN: the first database backup failed" "$out" rm -rf "$tdir" ublock="$(awk '/^install_update_check_timer\(\) \{/,/^}/' "$BS")" [ -n "$ublock" ] || { echo "FAIL: no install_update_check_timer found in $BS"; exit 1; } [ "$(printf '%s\n' "$ublock" | wc -l)" -lt 60 ] \ || { echo "FAIL: the extracted block is not install_update_check_timer -- did its closing brace move?"; exit 1; } udir="$(mktemp -d)" out="$(UPDATE_CHECK_SERVICE="$udir/felis-update-check.service" UPDATE_CHECK_TIMER="$udir/felis-update-check.timer" \ HOST_BIN=/usr/local/bin/felis STATE_DIR=/etc/felis bash -c ' ok() { printf "OK: %s\n" "$*"; }; warn() { printf "WARN: %s\n" "$*"; } systemctl() { printf "SYSTEMCTL: %s\n" "$*"; } '"$ublock"' install_update_check_timer' 2>&1)" unit="$(cat "$udir/felis-update-check.service")" timer="$(cat "$udir/felis-update-check.timer")" expect "the version check records its result for the panel" \ "ExecStart=/usr/local/bin/felis update --record -config /etc/felis/felis.host.toml" "$unit" expect "the version check is a oneshot" "Type=oneshot" "$unit" expect "the version check runs daily" "OnCalendar=*-*-* 05:30:00" "$timer" expect "a missed check catches up at boot" "Persistent=true" "$timer" expect "the version check timer is enabled" "SYSTEMCTL: enable --now felis-update-check.timer" "$out" expect "the first check runs without holding up the install" "SYSTEMCTL: start --no-block felis-update-check.service" "$out" expect "the install says where the result shows" "OK: version check: daily; the panel's Updates page" "$out" rm -rf "$udir" order="$(awk '/^main\(\) \{/,/^}/' "$BS" | grep -nE '^[[:space:]]*(install_velocity|install_update_check_timer)$' | tr '\n' ' ')" case "$order" in *install_velocity*install_update_check_timer*) echo "PASS the version check is installed after the proxy it reads" ;; *) echo "FAIL the version check must be installed after install_velocity: $order"; fails=$((fails + 1)) ;; esac wblock="$(awk '/^install_watchdog_timer\(\) \{/,/^}/' "$BS")" [ -n "$wblock" ] || { echo "FAIL: no install_watchdog_timer found in $BS"; exit 1; } [ "$(printf '%s\n' "$wblock" | wc -l)" -lt 60 ] \ Loading deploy/e2e_check.sh +1 −1 Changes for deploy/e2e_check.sh: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -46,7 +46,7 @@ for unit in k3s postgresql felis-velocity; do done # A release may predate a timer; what this commit installs has them all. if [ "$phase" != release ]; then for timer in felis-db-backup.timer felis-watchdog.timer; do for timer in felis-db-backup.timer felis-watchdog.timer felis-update-check.timer; do check "${timer} is scheduled" systemctl is-enabled --quiet "$timer" done fi Loading Loading
cmd/felis/update.go +85 −0 Changes for cmd/felis/update.go: 85 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -165,6 +165,7 @@ func cmdUpdate(args []string, stdout, stderr io.Writer) int { force := fs.Bool("force", false, "print the apply command for a selected component even when it is already up to date") velocityJar := fs.String("velocity-jar", updater.DefaultVelocityJarPath, "path to the installed Velocity jar to read the current version from") cfgPath := fs.String("config", "/etc/felis/felis.toml", "path to felis.toml, read for the maintenance window the panel stores") record := fs.Bool("record", false, "also store this check for the panel's Updates page (felis-update-check.timer runs it daily)") if err := fs.Parse(args); err != nil { return 2 } Loading Loading @@ -209,9 +210,93 @@ func cmdUpdate(args []string, stdout, stderr io.Writer) int { } fmt.Fprint(stdout, renderApplyGuidance(res, selected, *force)) } if *record { // A fresh context: the discovery pass may have spent most of updateTimeout. rctx, rcancel := context.WithTimeout(context.Background(), updateWindowTimeout) defer rcancel() if err := recordUpdateStatus(rctx, *cfgPath, buildStatusReport(res, src.Notes(), resolvedVersion(), now)); err != nil { fmt.Fprintf(stderr, "felis update: record the check for the panel: %v\n", err) return 1 } fmt.Fprint(stdout, "Recorded this check for the panel's Updates page.\n") } return 0 } // buildStatusReport turns one run into the record the panel shows: every planned // component in plan order, then each component whose installed version could not // be read, by name. A component the feed could not answer for is StateUnknown with // the reason, never StateCurrent: the panel must not call a component current when // nobody could check. func buildStatusReport(res updater.Result, notes map[string]string, felis string, now time.Time) updates.StatusReport { selectorOf := map[string]string{} for _, t := range updateTargets { if t.component != "" && selectorOf[t.component] == "" { selectorOf[t.component] = t.selector } } rep := updates.StatusReport{CheckedAt: now.UTC(), Felis: felis, Components: []updates.ComponentStatus{}} for _, a := range res.RunResult.Plan { cs := updates.ComponentStatus{ Name: a.Component, Current: a.Current.String(), Selector: selectorOf[a.Component], Note: notes[a.Component], } switch { case a.Kind == updates.ActionPinned: cs.State = updates.StatePinned case a.Kind == updates.ActionNotify || a.Kind == updates.ActionApply: cs.State = updates.StateAvailable cs.Latest = a.Latest.String() case a.LatestKnown: cs.State = updates.StateCurrent default: cs.State = updates.StateUnknown if err := res.RunResult.SourceErrors[a.Component]; err != nil { cs.Error = err.Error() } } rep.Components = append(rep.Components, cs) } names := make([]string, 0, len(res.GatherErrors)) for name := range res.GatherErrors { names = append(names, name) } sort.Strings(names) for _, name := range names { rep.Components = append(rep.Components, updates.ComponentStatus{ Name: name, State: updates.StateUnreadable, Selector: selectorOf[name], Note: notes[name], Error: res.GatherErrors[name].Error(), }) } return rep } // recordUpdateStatus upserts rep into platform_settings[updates.StatusKey], the // row the API serves to the panel's Updates page. func recordUpdateStatus(ctx context.Context, cfgPath string, rep updates.StatusReport) error { cfg, err := config.Load(cfgPath) if err != nil { return err } v, err := json.Marshal(rep) if err != nil { return err } conn, err := pgx.Connect(ctx, cfg.Database.URL) if err != nil { return err } defer conn.Close(context.Background()) _, err = conn.Exec(ctx, `INSERT INTO platform_settings (key, value) VALUES ($1, $2::jsonb) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, updates.StatusKey, string(v)) return err } // renderUpdateReport renders the component status table. With no selectors it shows // every tracked component; with selectors it shows only the components those // selectors name, so `felis update --velocity` is a focused answer rather than the Loading
cmd/felis/update_test.go +52 −0 Changes for cmd/felis/update_test.go: 52 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "encoding/json" "errors" "strings" "testing" "time" "felis.lolicon.best/internal/updater" "felis.lolicon.best/internal/updates" Loading Loading @@ -279,3 +281,53 @@ func TestInstallerRefNamesATag(t *testing.T) { t.Errorf("no felis-api row: installerRef = %q, want main", got) } } func mustVersion(t *testing.T, s string) updates.Version { t.Helper() v, err := updates.Parse(s) if err != nil { t.Fatalf("parse %q: %v", s, err) } return v } // The record the panel shows keeps every component with a state that cannot be // mistaken: a feed failure is "unknown" with its reason, an unreadable install is // listed after the plan, and each row carries the selector that prints its apply. func TestBuildStatusReport(t *testing.T) { res := planResult([]updates.Action{ {Component: "felis-api", Current: mustVersion(t, "v0.4.0"), Latest: mustVersion(t, "v0.5.0"), LatestKnown: true, Kind: updates.ActionNotify}, {Component: "velocity", Current: mustVersion(t, "3.4.0"), Latest: mustVersion(t, "3.4.0"), LatestKnown: true, Kind: updates.ActionNone}, {Component: "k3s", Current: mustVersion(t, "v1.36.2+k3s1"), Kind: updates.ActionNone}, {Component: "cloudflared", Current: mustVersion(t, "2026.6.1"), Latest: mustVersion(t, "2026.9.0"), LatestKnown: true, Kind: updates.ActionApply}, {Component: "mc-lobby", Current: mustVersion(t, "1.21.4"), Kind: updates.ActionPinned}, }) res.RunResult.SourceErrors["k3s"] = errors.New("github: HTTP 403") res.GatherErrors["postgresql"] = errors.New("psql: not found") res.GatherErrors["jre"] = errors.New("release file missing") notes := map[string]string{"postgresql": "PostgreSQL 13 is past its end of life", "velocity": "pinned minor 3.4"} now := time.Date(2026, 9, 25, 3, 4, 5, 0, time.FixedZone("CST", 8*3600)) b, err := json.Marshal(buildStatusReport(res, notes, "v0.4.0", now)) if err != nil { t.Fatal(err) } want := `{"checked_at":"2026-09-24T19:04:05Z","felis":"v0.4.0","components":[` + `{"name":"felis-api","current":"v0.4.0","latest":"v0.5.0","state":"available","selector":"panel"},` + `{"name":"velocity","current":"3.4.0","state":"current","selector":"velocity","note":"pinned minor 3.4"},` + `{"name":"k3s","current":"v1.36.2+k3s1","state":"unknown","selector":"k3s","error":"github: HTTP 403"},` + `{"name":"cloudflared","current":"2026.6.1","latest":"2026.9.0","state":"available","selector":"cloudflared"},` + `{"name":"mc-lobby","current":"1.21.4","state":"pinned"},` + `{"name":"jre","state":"unreadable","selector":"jre","error":"release file missing"},` + `{"name":"postgresql","state":"unreadable","selector":"postgres","note":"PostgreSQL 13 is past its end of life","error":"psql: not found"}]}` if string(b) != want { t.Errorf("status report =\n%s\nwant\n%s", b, want) } // Nothing tracked still records an empty list, so the panel can tell "checked, // nothing to show" from a report that never arrived. b, _ = json.Marshal(buildStatusReport(planResult(nil), nil, "v0.4.0", now)) if !strings.Contains(string(b), `"components":[]`) { t.Errorf("an empty check = %s, want an empty components list", b) } }
deploy/bootstrap.sh +44 −0 Changes for deploy/bootstrap.sh: 44 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -346,6 +346,8 @@ DB_BACKUP_SERVICE="/etc/systemd/system/felis-db-backup.service" DB_BACKUP_TIMER="/etc/systemd/system/felis-db-backup.timer" WATCHDOG_SERVICE="/etc/systemd/system/felis-watchdog.service" WATCHDOG_TIMER="/etc/systemd/system/felis-watchdog.timer" UPDATE_CHECK_SERVICE="/etc/systemd/system/felis-update-check.service" UPDATE_CHECK_TIMER="/etc/systemd/system/felis-update-check.timer" WATCHDOG_STATE="/var/lib/felis/watchdog/state.json" OFFSITE_ENV="${STATE_DIR}/offsite.env" OFFSITE_SERVICE="/etc/systemd/system/felis-offsite.service" Loading Loading @@ -3262,6 +3264,46 @@ summary_offsite() { # memory, and mails the owners (their verified addresses, over the [smtp] relay) what # has stayed wrong long enough to matter. It runs on the host so a k3s that is down is # still reported. The first run happens now, so a broken unit shows up in this install. # The daily version check. Felis applies no update on its own; `felis update --record` # compares what this host runs with the newest upstream releases and stores the result, # which the panel's Updates page shows with the command that applies each update. It runs # on the host because that is where the installed versions are readable. The first check # runs in the background: it waits on the release feeds, and nothing in the install # depends on it. install_update_check_timer() { cat > "$UPDATE_CHECK_SERVICE" <<EOF [Unit] Description=Felis component version check (felis update --record) After=network-online.target postgresql.service k3s.service Wants=network-online.target [Service] Type=oneshot ExecStart=${HOST_BIN} update --record -config ${STATE_DIR}/felis.host.toml TimeoutStartSec=5min Nice=10 PrivateTmp=yes NoNewPrivileges=yes ProtectSystem=full EOF cat > "$UPDATE_CHECK_TIMER" <<EOF [Unit] Description=Daily Felis component version check [Timer] OnCalendar=*-*-* 05:30:00 RandomizedDelaySec=30min Persistent=true [Install] WantedBy=timers.target EOF systemctl daemon-reload systemctl enable --now felis-update-check.timer systemctl start --no-block felis-update-check.service ok "version check: daily; the panel's Updates page shows what has a newer release (journalctl -u felis-update-check)" } install_watchdog_timer() { local disks="/,/var/lib/rancher/k3s,/var/lib/postgresql,/var/lib/felis" path for path in "$FELIS_WORLDS_HOST_PATH" "$FELIS_ARCHIVE_LOCAL_PATH" "$FELIS_DB_BACKUP_DIR"; do Loading Loading @@ -4216,6 +4258,8 @@ main() { install_db_backup_timer # After the backup timer: its first bundle is part of the first copy. install_offsite_timer # After install_velocity: the check reads the installed proxy jar's version. install_update_check_timer # Last: its first run should see the platform as this install leaves it. install_watchdog_timer mark_bootstrap_done Loading
deploy/bootstrap_test.sh +28 −0 Changes for deploy/bootstrap_test.sh: 28 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1530,6 +1530,34 @@ expect "a failed first backup shows its log" "JOURNAL: pg_dump: connection refus expect "a failed first backup is a loud warning" "WARN: the first database backup failed" "$out" rm -rf "$tdir" ublock="$(awk '/^install_update_check_timer\(\) \{/,/^}/' "$BS")" [ -n "$ublock" ] || { echo "FAIL: no install_update_check_timer found in $BS"; exit 1; } [ "$(printf '%s\n' "$ublock" | wc -l)" -lt 60 ] \ || { echo "FAIL: the extracted block is not install_update_check_timer -- did its closing brace move?"; exit 1; } udir="$(mktemp -d)" out="$(UPDATE_CHECK_SERVICE="$udir/felis-update-check.service" UPDATE_CHECK_TIMER="$udir/felis-update-check.timer" \ HOST_BIN=/usr/local/bin/felis STATE_DIR=/etc/felis bash -c ' ok() { printf "OK: %s\n" "$*"; }; warn() { printf "WARN: %s\n" "$*"; } systemctl() { printf "SYSTEMCTL: %s\n" "$*"; } '"$ublock"' install_update_check_timer' 2>&1)" unit="$(cat "$udir/felis-update-check.service")" timer="$(cat "$udir/felis-update-check.timer")" expect "the version check records its result for the panel" \ "ExecStart=/usr/local/bin/felis update --record -config /etc/felis/felis.host.toml" "$unit" expect "the version check is a oneshot" "Type=oneshot" "$unit" expect "the version check runs daily" "OnCalendar=*-*-* 05:30:00" "$timer" expect "a missed check catches up at boot" "Persistent=true" "$timer" expect "the version check timer is enabled" "SYSTEMCTL: enable --now felis-update-check.timer" "$out" expect "the first check runs without holding up the install" "SYSTEMCTL: start --no-block felis-update-check.service" "$out" expect "the install says where the result shows" "OK: version check: daily; the panel's Updates page" "$out" rm -rf "$udir" order="$(awk '/^main\(\) \{/,/^}/' "$BS" | grep -nE '^[[:space:]]*(install_velocity|install_update_check_timer)$' | tr '\n' ' ')" case "$order" in *install_velocity*install_update_check_timer*) echo "PASS the version check is installed after the proxy it reads" ;; *) echo "FAIL the version check must be installed after install_velocity: $order"; fails=$((fails + 1)) ;; esac wblock="$(awk '/^install_watchdog_timer\(\) \{/,/^}/' "$BS")" [ -n "$wblock" ] || { echo "FAIL: no install_watchdog_timer found in $BS"; exit 1; } [ "$(printf '%s\n' "$wblock" | wc -l)" -lt 60 ] \ Loading
deploy/e2e_check.sh +1 −1 Changes for deploy/e2e_check.sh: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -46,7 +46,7 @@ for unit in k3s postgresql felis-velocity; do done # A release may predate a timer; what this commit installs has them all. if [ "$phase" != release ]; then for timer in felis-db-backup.timer felis-watchdog.timer; do for timer in felis-db-backup.timer felis-watchdog.timer felis-update-check.timer; do check "${timer} is scheduled" systemctl is-enabled --quiet "$timer" done fi Loading