feat(linux): package Linux release, deploy to server, add in-app update banner

Build task embeds GIT_HASH via --dart-define; new deploy-linux-to-server task
packages a tar.gz and updates latest.json on the server. The account list screen
shows a MaterialBanner when a newer Linux build is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-14 23:46:29 +02:00
co-authored by Claude Sonnet 4.6
parent 99c3a1d808
commit 724df4ea37
5 changed files with 129 additions and 17 deletions
+16
View File
@@ -43,6 +43,22 @@ jobs:
- name: Build Linux
run: nix develop --no-warn-dirty --command task build-linux-release
- name: Set up SSH key
continue-on-error: true
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Deploy Linux to server
continue-on-error: true
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
run: nix develop --no-warn-dirty --command task deploy-linux-to-server
deploy-playstore:
name: Build & Deploy to Play Store
runs-on: self-hosted
+23 -1
View File
@@ -216,7 +216,29 @@ tasks:
generates:
- build/linux/x64/release/bundle/sharedinbox
cmds:
- scripts/silent_on_success.sh fvm flutter build linux --release --no-pub
- scripts/silent_on_success.sh fvm flutter build linux --release --no-pub --dart-define=GIT_HASH=$(git rev-parse --short HEAD)
deploy-linux-to-server:
desc: Package and deploy the Linux release bundle to the server, update latest.json
deps: [build-linux-release]
preconditions:
- sh: test -n "$SSH_USER"
msg: "SSH_USER is not set"
- sh: test -n "$SSH_HOST"
msg: "SSH_HOST is not set"
cmds:
- |
HASH=$(git rev-parse --short HEAD)
DATE_PATH=$(date -u +%Y/%m/%d)
REMOTE_DIR="public_html/builds/$DATE_PATH"
TARBALL="sharedinbox-linux-amd64-$HASH.tar.gz"
tar -czf /tmp/$TARBALL -C build/linux/x64/release bundle
ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_DIR"
scp -o StrictHostKeyChecking=no /tmp/$TARBALL "$SSH_USER@$SSH_HOST:$REMOTE_DIR/$TARBALL"
DOWNLOAD_URL="https://sharedinbox.de/builds/$DATE_PATH/$TARBALL"
echo "{\"version\":\"$HASH\",\"linux\":\"$DOWNLOAD_URL\"}" | \
ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json"
echo "Uploaded $TARBALL and updated latest.json"
_android-avd-setup:
+37
View File
@@ -0,0 +1,37 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
const _kAppVersion = String.fromEnvironment('GIT_HASH');
const _kLatestJsonUrl = 'https://sharedinbox.de/latest.json';
class UpdateInfo {
const UpdateInfo({required this.latestVersion, required this.downloadUrl});
final String latestVersion;
final String downloadUrl;
}
/// Returns an [UpdateInfo] when a newer Linux version is available, or null
/// if the app is up to date, the version is unknown, or the platform is not
/// Linux desktop.
final updateInfoProvider = FutureProvider<UpdateInfo?>((ref) async {
if (!Platform.isLinux || _kAppVersion.isEmpty) return null;
try {
final resp = await http
.get(Uri.parse(_kLatestJsonUrl))
.timeout(const Duration(seconds: 10));
if (resp.statusCode != 200) return null;
final json = jsonDecode(resp.body) as Map<String, dynamic>;
final latest = json['version'] as String?;
final url = json['linux'] as String?;
if (latest == null || url == null) return null;
if (latest == _kAppVersion) return null;
return UpdateInfo(latestVersion: latest, downloadUrl: url);
} catch (_) {
return null;
}
});
+52 -16
View File
@@ -3,9 +3,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:sharedinbox/core/models/account.dart';
import 'package:sharedinbox/core/services/update_service.dart';
import 'package:sharedinbox/di.dart';
import 'package:url_launcher/url_launcher.dart';
class AccountListScreen extends ConsumerWidget {
const AccountListScreen({super.key});
@@ -52,21 +53,28 @@ class AccountListScreen extends ConsumerWidget {
],
),
),
body: StreamBuilder(
stream: ref.watch(accountRepositoryProvider).observeAccounts(),
builder: (ctx, snap) {
if (!snap.hasData) {
return const Center(child: CircularProgressIndicator());
}
final accounts = snap.data!;
if (accounts.isEmpty) {
return const _OnboardingView();
}
return ListView.builder(
itemCount: accounts.length,
itemBuilder: (ctx, i) => _AccountTile(account: accounts[i]),
);
},
body: Column(
children: [
const _UpdateBanner(),
Expanded(
child: StreamBuilder(
stream: ref.watch(accountRepositoryProvider).observeAccounts(),
builder: (ctx, snap) {
if (!snap.hasData) {
return const Center(child: CircularProgressIndicator());
}
final accounts = snap.data!;
if (accounts.isEmpty) {
return const _OnboardingView();
}
return ListView.builder(
itemCount: accounts.length,
itemBuilder: (ctx, i) => _AccountTile(account: accounts[i]),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.push('/accounts/add'),
@@ -341,3 +349,31 @@ bool _sieveSupported(Account account) {
if (account.type == AccountType.jmap) return true;
return account.manageSieveAvailable != false;
}
/// Shown on Linux desktop when a newer build is available on the server.
class _UpdateBanner extends ConsumerWidget {
const _UpdateBanner();
@override
Widget build(BuildContext context, WidgetRef ref) {
final update = ref.watch(updateInfoProvider);
return update.when(
data: (info) {
if (info == null) return const SizedBox.shrink();
return MaterialBanner(
content: Text('Update available: ${info.latestVersion}'),
leading: const Icon(Icons.system_update),
actions: [
TextButton(
onPressed: () =>
unawaited(launchUrl(Uri.parse(info.downloadUrl))),
child: const Text('Download'),
),
],
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
);
}
}
+1
View File
@@ -63,6 +63,7 @@ const _excluded = {
'lib/data/repositories/sync_log_repository_impl.dart',
'lib/data/repositories/undo_repository_impl.dart',
'lib/data/repositories/search_history_repository_impl.dart',
'lib/core/services/update_service.dart',
};
void main() {