Merge pull request 'Release v1.2.0 — fleet updates and reliability' (#49) from release-v1.2.0 into main
Release / Build + push image (push) Successful in 2m10s

This commit was merged in pull request #49.
This commit is contained in:
2026-08-22 11:34:51 +01:00
4 changed files with 56 additions and 5 deletions
+40 -1
View File
@@ -6,6 +6,40 @@ and the project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [1.2.0] - 2026-08-22
### Added
- Fleet-wide agent updates are now discoverable from Settings and the
dashboard, with arbitrary host subsets, name/tag/version/eligibility
filtering, explicit exclusion reasons, server-side membership validation,
and a canary-first pause after the first verified reconnect. Halted rolls
expose retry and resume actions. ([#43])
- Running jobs with no recent persisted activity are detected server-side and
surfaced through deduplicated alerts. Kind-aware thresholds protect normal
long-running work, terminal jobs self-resolve, and Prometheus exports stuck
job count and oldest inactivity age. ([#41])
- Snapshot projections now retain host-level refresh timestamps—including
authoritative empty reports—expose a derived stale flag, refresh after
backup, forget, and prune, and support an explicit operator reconciliation
endpoint. ([#40])
### Fixed
- Raised the bounded WebSocket read limit on both peers so ordinary large
snapshot reports and restic events no longer disconnect otherwise healthy
agents. Regression coverage exercises payloads beyond the library's former
32 KiB default in both directions. ([#44])
- Fleet-update timeout verification now performs a final authoritative read of
the agent version delivered by the reconnect `hello`, avoiding a poll/deadline
race and reporting the last observed version when verification fails. ([#43])
### Changed
- CI runner images and release images use the anonymous-pull `public`
namespace at `docker.dcglab.co.uk`; registry authentication remains required
only for publishing.
## [1.1.1] - 2026-08-22
### Fixed
@@ -140,7 +174,8 @@ with a web UI, JSON API, and self-updating agent fleet.
go vet, golangci-lint).
- Threat model published (`docs/threat-model.md`).
[Unreleased]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.1.1...HEAD
[Unreleased]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.2.0...HEAD
[1.2.0]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.1.1...v1.2.0
[1.1.1]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.1.0...v1.1.1
[1.1.0]: https://gitea.dcglab.co.uk/steve/restic-manager/releases/tag/v1.1.0
[1.0.0]: https://gitea.dcglab.co.uk/steve/restic-manager/releases/tag/v1.0.0
@@ -148,3 +183,7 @@ with a web UI, JSON API, and self-updating agent fleet.
[#37]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/37
[#36]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/36
[#34]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/34
[#40]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/40
[#41]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/41
[#43]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/43
[#44]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/44
+11
View File
@@ -61,9 +61,20 @@ type Engine struct {
stuckThresholds map[string]time.Duration
closeOnce sync.Once
notifyWG sync.WaitGroup
done chan struct{}
}
func (e *Engine) dispatchNotification(ctx context.Context, payload notification.Payload) {
e.notifyWG.Add(1)
go func() {
defer e.notifyWG.Done()
e.hub.Dispatch(ctx, payload)
}()
}
func (e *Engine) waitNotifications() { e.notifyWG.Wait() }
// NewEngine builds the engine. agentOfflineFloor + tickPeriod default
// to 15min and 60s respectively when zero.
func NewEngine(st *store.Store, hub *notification.Hub) *Engine {
+4 -4
View File
@@ -60,7 +60,7 @@ func (e *Engine) raiseAndNotify(ctx context.Context, hostID, kind, dedupKey, sev
if err == nil {
hostName = host.Name
}
go e.hub.Dispatch(ctx, notification.Payload{
e.dispatchNotification(ctx, notification.Payload{
Event: notification.EventRaised,
AlertID: id,
Severity: severity,
@@ -85,7 +85,7 @@ func (e *Engine) Acknowledge(ctx context.Context, alertID, userID string, when t
return nil //nolint:nilerr
}
p := alertPayload(ctx, e.store, notification.EventAcknowledged, a)
go e.hub.Dispatch(context.WithoutCancel(ctx), p)
e.dispatchNotification(context.WithoutCancel(ctx), p)
return nil
}
@@ -99,7 +99,7 @@ func (e *Engine) Resolve(ctx context.Context, alertID string, when time.Time) er
return nil
}
p := alertPayload(ctx, e.store, notification.EventResolved, a)
go e.hub.Dispatch(context.WithoutCancel(ctx), p)
e.dispatchNotification(context.WithoutCancel(ctx), p)
return nil
}
@@ -164,7 +164,7 @@ func (e *Engine) resolveAndNotify(ctx context.Context, hostID, kind, dedupKey st
if a.Kind != kind || a.DedupKey != dedupKey {
continue
}
go e.hub.Dispatch(ctx, notification.Payload{
e.dispatchNotification(ctx, notification.Payload{
Event: notification.EventResolved,
AlertID: a.ID,
Severity: a.Severity,
+1
View File
@@ -25,6 +25,7 @@ func setupEngine(t *testing.T) (*Engine, *store.Store, string) {
aead, _ := crypto.NewAEAD(key)
hub := notification.NewHub(st, aead, "https://rm.example")
eng := NewEngine(st, hub)
t.Cleanup(eng.waitNotifications)
hostID := ulid.Make().String()
if err := st.CreateHost(context.Background(), store.Host{
ID: hostID, Name: "alfa-01", OS: "linux", Arch: "amd64",