OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-sidecar commits / d2fdc44

0.2.63: autoupdate skips a release pgh already staged; bundles pgh 0.1.157
pgh 0.1.157 never swaps a running app's tree: the install stages it as <name>.next, applied at the app's next start. The live manifest still names the old version, so after a sidecar restart every update tick would rebuild and re-stage the same tree. Candidates now carry the staged manifest's version, and a release equal to it is skipped. test/autoupdate-staged.test.js (red without the check: the staged release was installed again). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d2fdc446b1ca2a13a3e45f24909a1bd24ec459a9
by plankatron <fleeky@gmail.com> · 2026-10-02T01:41:02.000Z

Changed files (3) +71 -2

lib/index.js +17 -1
……@@ -739,7 +739,7 @@ export class Sidecar {
739739 if (candidates.length === 0) return
740740 const registries = [this._registry, ...this._releaseReplicas.map(r => r.registry)]
741741 for (const cand of candidates) {
742 const { name, currentVersion, currentTreeHash, registered, udsPath } = cand
742 const { name, currentVersion, currentTreeHash, stagedVersion, registered, udsPath } = cand
743743 let best = null
744744 for (const reg of registries) {
745745 try {
……@@ -768,6 +768,13 @@ export class Sidecar {
768768 this._lastNotifiedVersion.set(name, token)
769769 continue
770770 }
771 // Already STAGED: the app was running when this version was installed, so
772 // pgh left it as <name>.next to apply at the app's next launch (pgh
773 // materializers/atomic.js). Re-installing would only rebuild the same tree.
774 if (stagedVersion && best.version && stagedVersion === best.version) {
775 this._lastNotifiedVersion.set(name, token)
776 continue
777 }
771778
772779 // Auto-install new release (skip if user explicitly disabled).
773780 const autoupdate = this._isAutoupdateEnabled(name)
……@@ -869,6 +876,7 @@ export class Sidecar {
869876 name,
870877 currentVersion: version,
871878 currentTreeHash: treeHash,
879 stagedVersion: this._stagedVersion(name, readInstallManifest),
872880 registered: true,
873881 udsPath: app.udsPath
874882 })
……@@ -882,6 +890,7 @@ export class Sidecar {
882890 name: n,
883891 currentVersion: m.version || null,
884892 currentTreeHash: m.treeHash || null,
893 stagedVersion: this._stagedVersion(n, readInstallManifest),
885894 registered: false,
886895 udsPath: null
887896 })
……@@ -894,6 +903,13 @@ export class Sidecar {
894903 return [...seen.values()]
895904 }
896905
906 // Version of an update pgh STAGED because the app was running (<name>.next,
907 // which carries its own install manifest), or null.
908 _stagedVersion (name, readInstallManifest) {
909 if (!readInstallManifest) return null
910 try { return (readInstallManifest(name + '.next') || {}).version || null } catch { return null }
911 }
912
897913 // Admin setting, apps.json `bootstrapServeAllArches`. Unlike autoupdate this
898914 // defaults to FALSE: holding another arch's ~150MB tarball only pays off on a
899915 // machine that actually answers cold-start HTTPS /bootstrap fetches, i.e. a
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh-sidecar",
3 "version": "0.2.62",
3 "version": "0.2.63",
44 "description": "Small optional user-level daemon for pgh \u2014 shared identity vault, OTA polling, device linking, bootstrap seed, NameRegistry host",
55 "type": "module",
66 "main": "lib/index.js",
test/autoupdate-staged.test.js +53 -0
……@@ -0,0 +1,53 @@
1/**
2 * Autoupdate does not re-install a version pgh already STAGED.
3 *
4 * pgh never swaps a running app's tree: an install that finds the app running
5 * leaves the new tree as <name>.next, applied at the app's next launch (pgh
6 * lib/materializers/atomic.js). The live manifest still says the old version,
7 * so after a sidecar restart (empty _lastNotifiedVersion) every update tick
8 * would rebuild and re-stage the same tree. The staged manifest's version is
9 * the tie-breaker.
10 */
11import test from 'brittle'
12import { Sidecar } from '../lib/index.js'
13
14function stub (cand) {
15 const s = Object.create(Sidecar.prototype)
16 s._installFailures = new Map()
17 s._installInFlight = new Set()
18 s._lastNotifiedVersion = new Map()
19 s._releaseReplicas = []
20 s._registry = { latest: async () => ({ version: '2', treeHash: 't2', publishedAt: 1 }) }
21 s._listUpdateCandidates = async () => [cand]
22 s._isAutoupdateEnabled = () => true
23 s._reconcileBootstrap = async () => {}
24 s.installs = []
25 s._installRelease = async (name, best) => { s.installs.push(name + '@' + best.version) }
26 return s
27}
28
29test('a release already staged for the running app is not installed again', async (t) => {
30 const s = stub({ name: 'app', currentVersion: '1', currentTreeHash: 't1', stagedVersion: '2', registered: false })
31 await s._notifyAppsOfNewReleases()
32 t.alike(s.installs, [], 'no install: version 2 is already staged')
33})
34
35test('a newer release than the staged one is installed', async (t) => {
36 const s = stub({ name: 'app', currentVersion: '1', currentTreeHash: 't1', stagedVersion: '1.5', registered: false })
37 await s._notifyAppsOfNewReleases()
38 t.alike(s.installs, ['app@2'])
39})
40
41test('nothing staged: installed as before', async (t) => {
42 const s = stub({ name: 'app', currentVersion: '1', currentTreeHash: 't1', stagedVersion: null, registered: false })
43 await s._notifyAppsOfNewReleases()
44 t.alike(s.installs, ['app@2'])
45})
46
47test('_stagedVersion reads <name>.next\'s manifest', (t) => {
48 const s = Object.create(Sidecar.prototype)
49 const read = (n) => (n === 'app.next' ? { version: '2' } : null)
50 t.is(s._stagedVersion('app', read), '2')
51 t.is(s._stagedVersion('other', read), null)
52 t.is(s._stagedVersion('app', null), null)
53})