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:
co-authored by
Claude Sonnet 4.6
parent
99c3a1d808
commit
724df4ea37
@@ -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
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user