forked from Foxyz/sync_replay_project
Updater: update channel Stable / Dev in Settings → Updates; Dev stays on test builds after a release
Dev reads the dev and stable manifests and offers the newer by semver; a test build stores Dev on its first start, so the release that updates it keeps the channel. Switching to Stable in a test build asks before going back to an older release. tool/dev_release.sh's one-time command also writes the update-channel file that sets Dev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
69fae18fdc
commit
f2721d125c
11 files changed
+364
-28
No files matched your search
@@ -237,16 +237,58 @@ Future<UpdateManifest> fetchManifest(
|
||||
/// A test build (`1.1.3-dev.4`); `0.0.0-dev` (a dev run) is not one.
|
||||
bool isDevBuild(String version) => version.contains('-dev.');
|
||||
|
||||
/// The manifest to update from: the stable one, and for a [isDevBuild]
|
||||
/// also the dev one, whichever is newer (either alone if the other fails).
|
||||
/// A release outranks its own test builds, so testers return to stable.
|
||||
/// Where an install takes its updates from. [stable]: releases only.
|
||||
/// [dev]: test builds as well (`devManifestUrl`), whichever of the two is
|
||||
/// newer, so a release newer than the running test build is installed and
|
||||
/// the next test line after it still is.
|
||||
enum UpdateChannel {
|
||||
stable,
|
||||
dev;
|
||||
|
||||
static UpdateChannel? fromName(Object? name) {
|
||||
for (final c in values) {
|
||||
if (c.name == name) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// The channel of an install that never chose one: [UpdateChannel.dev]
|
||||
/// for a test build, else [UpdateChannel.stable].
|
||||
UpdateChannel defaultUpdateChannel(String version) =>
|
||||
isDevBuild(version) ? UpdateChannel.dev : UpdateChannel.stable;
|
||||
|
||||
/// Of the stable and dev manifests read (null: that one failed), the one
|
||||
/// [channel] updates from: on stable the stable one, on dev the newer of
|
||||
/// the two by semver (a release outranks its own test builds).
|
||||
UpdateManifest? pickManifest(UpdateChannel channel,
|
||||
{UpdateManifest? stable, UpdateManifest? dev}) {
|
||||
if (channel == UpdateChannel.stable || dev == null) return stable;
|
||||
if (stable == null) return dev;
|
||||
return isNewerVersion(stable.version, dev.version) ? dev : stable;
|
||||
}
|
||||
|
||||
/// A stable [latest] that is older than the running test build
|
||||
/// [currentVersion]: on [UpdateChannel.stable] it is offered only after
|
||||
/// asking ("go back to stable X?"), never as an update.
|
||||
bool isStableBehind(String currentVersion, UpdateManifest latest) =>
|
||||
isDevBuild(currentVersion) &&
|
||||
!isDevBuild(latest.version) &&
|
||||
compareVersions(latest.version, currentVersion) < 0;
|
||||
|
||||
/// The manifest to update from on [channel] (default by [currentVersion]:
|
||||
/// [defaultUpdateChannel]): the stable one, and on dev also the dev one,
|
||||
/// the newer of the two ([pickManifest]; either alone if the other fails).
|
||||
Future<UpdateManifest> fetchLatestManifest(
|
||||
{HttpClient? http,
|
||||
String currentVersion = appVersion,
|
||||
UpdateChannel? channel,
|
||||
String url = updateManifestUrl,
|
||||
String devUrl = devManifestUrl}) async {
|
||||
final stable = fetchManifest(http: http, url: url);
|
||||
if (!isDevBuild(currentVersion)) return stable;
|
||||
if ((channel ?? defaultUpdateChannel(currentVersion)) == UpdateChannel.stable) {
|
||||
return stable;
|
||||
}
|
||||
stable.ignore(); // awaited below; don't let an early failure go uncaught
|
||||
final dev = await fetchManifest(http: http, url: devUrl)
|
||||
.then<UpdateManifest?>((m) => m, onError: (Object e) {
|
||||
@@ -254,12 +296,8 @@ Future<UpdateManifest> fetchLatestManifest(
|
||||
return null;
|
||||
});
|
||||
if (dev == null) return stable;
|
||||
try {
|
||||
final s = await stable;
|
||||
return isNewerVersion(s.version, dev.version) ? dev : s;
|
||||
} catch (_) {
|
||||
return dev;
|
||||
}
|
||||
final s = await stable.then<UpdateManifest?>((m) => m, onError: (Object _) => null);
|
||||
return pickManifest(UpdateChannel.dev, stable: s, dev: dev)!;
|
||||
}
|
||||
|
||||
typedef DownloadProgress = void Function(int received, int? total);
|
||||
|
||||
@@ -278,21 +278,47 @@ void main() {
|
||||
await req.response.close();
|
||||
});
|
||||
final base = 'http://127.0.0.1:${http.port}';
|
||||
Future<String> latest(String current) async => (await fetchLatestManifest(
|
||||
currentVersion: current, url: '$base/stable', devUrl: '$base/dev'))
|
||||
Future<String> latest(String current, [UpdateChannel? channel]) async => (await fetchLatestManifest(
|
||||
currentVersion: current, channel: channel, url: '$base/stable', devUrl: '$base/dev'))
|
||||
.version;
|
||||
try {
|
||||
expect(await latest('1.1.2'), '1.1.2'); // stable builds never see dev
|
||||
expect(await latest('1.1.3-dev.1'), '1.1.3-dev.2');
|
||||
versions['/stable'] = '1.1.3'; // the release outranks its test builds
|
||||
expect(await latest('1.1.3-dev.2'), '1.1.3');
|
||||
// A release installed on the dev channel still sees the next test line.
|
||||
versions['/dev'] = '1.1.4-dev.1';
|
||||
expect(await latest('1.1.3', UpdateChannel.dev), '1.1.4-dev.1');
|
||||
expect(await latest('1.1.3', UpdateChannel.stable), '1.1.3');
|
||||
expect(await latest('1.1.4-dev.1', UpdateChannel.stable), '1.1.3');
|
||||
versions.remove('/stable');
|
||||
expect(await latest('1.1.3-dev.1'), '1.1.3-dev.2');
|
||||
expect(await latest('1.1.3-dev.1'), '1.1.4-dev.1');
|
||||
} finally {
|
||||
await http.close(force: true);
|
||||
}
|
||||
});
|
||||
|
||||
test('update channel: default, pick by semver, a stable behind a test build', () {
|
||||
UpdateManifest m(String v) => UpdateManifest(version: v, protocol: 1, url: 'http://x/$v', notes: '');
|
||||
expect(defaultUpdateChannel('1.4.0-dev.3'), UpdateChannel.dev);
|
||||
expect(defaultUpdateChannel('1.4.0'), UpdateChannel.stable);
|
||||
expect(defaultUpdateChannel('0.0.0-dev'), UpdateChannel.stable);
|
||||
expect(UpdateChannel.fromName('dev'), UpdateChannel.dev);
|
||||
expect(UpdateChannel.fromName('beta'), isNull);
|
||||
String? pick(UpdateChannel ch, String? s, String? d) =>
|
||||
pickManifest(ch, stable: s == null ? null : m(s), dev: d == null ? null : m(d))?.version;
|
||||
expect(pick(UpdateChannel.dev, '1.4.0', '1.4.0-dev.3'), '1.4.0');
|
||||
expect(pick(UpdateChannel.dev, '1.4.0', '1.4.1-dev.1'), '1.4.1-dev.1');
|
||||
expect(pick(UpdateChannel.dev, '1.3.2', '1.4.0-dev.3'), '1.4.0-dev.3');
|
||||
expect(pick(UpdateChannel.dev, null, '1.4.0-dev.3'), '1.4.0-dev.3');
|
||||
expect(pick(UpdateChannel.dev, '1.3.2', null), '1.3.2');
|
||||
expect(pick(UpdateChannel.stable, '1.3.2', '1.4.0-dev.3'), '1.3.2');
|
||||
expect(isStableBehind('1.4.0-dev.3', m('1.3.2')), isTrue);
|
||||
expect(isStableBehind('1.4.0-dev.3', m('1.4.0')), isFalse);
|
||||
expect(isStableBehind('1.3.2', m('1.3.1')), isFalse); // not a test build
|
||||
expect(isStableBehind('1.4.0-dev.3', m('1.4.0-dev.2')), isFalse);
|
||||
});
|
||||
|
||||
group('upgrade flow', () {
|
||||
late Directory work;
|
||||
late _Server server;
|
||||
|
||||
Reference in new issue
Block a user